diff --git a/marklogic/client.py b/marklogic/client.py index 3ba4a10..23a87e1 100644 --- a/marklogic/client.py +++ b/marklogic/client.py @@ -1,6 +1,7 @@ import requests from marklogic.cloud_auth import MarkLogicCloudAuth from marklogic.documents import DocumentManager +from marklogic.rows import RowManager from requests.auth import HTTPDigestAuth from urllib.parse import urljoin @@ -70,3 +71,9 @@ def documents(self): if not hasattr(self, "_documents"): self._documents = DocumentManager(self) return self._documents + + @property + def rows(self): + if not hasattr(self, "_rows"): + self._rows = RowManager(self) + return self._rows diff --git a/marklogic/rows.py b/marklogic/rows.py new file mode 100644 index 0000000..da47252 --- /dev/null +++ b/marklogic/rows.py @@ -0,0 +1,43 @@ +import json +from requests import Session + +""" +Defines a RowManager class to simplify usage of the "/v1/rows" & "/v1/rows/graphql" REST +endpoints defined at https://docs.marklogic.com/REST/POST/v1/rows/graphql +""" + + +class RowManager: + """ + Provides a method to simplify sending a GraphQL request to the GraphQL rows endpoint. + """ + def __init__(self, session: Session): + self._session = session + + def graphql(self, graphql_query, return_response=False, *args, **kwargs): + """ + Send a GraphQL query to MarkLogic via a POST to the endpoint defined at + https://docs.marklogic.com/REST/POST/v1/rows/graphql + + :param graphql_query: a GraphQL query string. Note - this is the query string + only, not the entire query JSON object. See the following for more information: + https://spec.graphql.org/October2021/#sec-Overview + https://graphql.org/learn/queries/ + :param return_response: boolean specifying if the entire original response + object should be returned (True) or if only the data should be returned (False) + upon a success (2xx) response. Note that if the status code of the response is + not 2xx, then the entire response is always returned. + """ + headers = kwargs.pop("headers", {}) + headers["Content-Type"] = "application/graphql" + response = self._session.post( + "v1/rows/graphql", + headers=headers, + data=json.dumps({"query": graphql_query}), + **kwargs + ) + return ( + response.json() + if response.status_code == 200 and not return_response + else response + ) \ No newline at end of file diff --git a/test-app/src/main/ml-config/databases/content-database.json b/test-app/src/main/ml-config/databases/content-database.json new file mode 100644 index 0000000..593bca4 --- /dev/null +++ b/test-app/src/main/ml-config/databases/content-database.json @@ -0,0 +1,4 @@ +{ + "database-name": "%%DATABASE%%", + "schema-database": "%%SCHEMAS_DATABASE%%" +} \ No newline at end of file diff --git a/test-app/src/main/ml-config/databases/modules-database.json b/test-app/src/main/ml-config/databases/modules-database.json new file mode 100644 index 0000000..f0112d1 --- /dev/null +++ b/test-app/src/main/ml-config/databases/modules-database.json @@ -0,0 +1,3 @@ +{ + "database-name": "%%MODULES_DATABASE%%" +} \ No newline at end of file diff --git a/test-app/src/main/ml-config/databases/schemas-database.json b/test-app/src/main/ml-config/databases/schemas-database.json new file mode 100644 index 0000000..f0f14c7 --- /dev/null +++ b/test-app/src/main/ml-config/databases/schemas-database.json @@ -0,0 +1,3 @@ +{ + "database-name": "%%SCHEMAS_DATABASE%%" +} \ No newline at end of file diff --git a/test-app/src/main/ml-data/collections.properties b/test-app/src/main/ml-data/collections.properties index a777f44..48a99e2 100644 --- a/test-app/src/main/ml-data/collections.properties +++ b/test-app/src/main/ml-data/collections.properties @@ -1 +1 @@ -*=test-data +*=test-data,search-test \ No newline at end of file diff --git a/test-app/src/main/ml-data/musicians/collections.properties b/test-app/src/main/ml-data/musicians/collections.properties new file mode 100644 index 0000000..a777f44 --- /dev/null +++ b/test-app/src/main/ml-data/musicians/collections.properties @@ -0,0 +1 @@ +*=test-data diff --git a/test-app/src/main/ml-data/musicians/musician1.json b/test-app/src/main/ml-data/musicians/musician1.json new file mode 100644 index 0000000..5ce02ba --- /dev/null +++ b/test-app/src/main/ml-data/musicians/musician1.json @@ -0,0 +1,11 @@ +{ + "musician": { + "lastName": "Armstrong", + "firstName": "Louis", + "dob": "1901-08-04", + "instrument": [ + "trumpet", + "vocal" + ] + } +} \ No newline at end of file diff --git a/test-app/src/main/ml-data/musicians/musician2.json b/test-app/src/main/ml-data/musicians/musician2.json new file mode 100644 index 0000000..ab35baa --- /dev/null +++ b/test-app/src/main/ml-data/musicians/musician2.json @@ -0,0 +1,11 @@ +{ + "musician": { + "lastName": "Byron", + "firstName": "Don", + "dob": "1958-11-08", + "instrument": [ + "clarinet", + "saxophone" + ] + } +} \ No newline at end of file diff --git a/test-app/src/main/ml-data/musicians/musician3.json b/test-app/src/main/ml-data/musicians/musician3.json new file mode 100644 index 0000000..bdf9c59 --- /dev/null +++ b/test-app/src/main/ml-data/musicians/musician3.json @@ -0,0 +1,10 @@ +{ + "musician": { + "lastName": "Coltrane", + "firstName": "John", + "dob": "1926-09-23", + "instrument": [ + "saxophone" + ] + } +} \ No newline at end of file diff --git a/test-app/src/main/ml-data/musicians/musician4.json b/test-app/src/main/ml-data/musicians/musician4.json new file mode 100644 index 0000000..567bff4 --- /dev/null +++ b/test-app/src/main/ml-data/musicians/musician4.json @@ -0,0 +1,10 @@ +{ + "musician": { + "lastName": "Davis", + "firstName": "Miles", + "dob": "1926-05-26", + "instrument": [ + "trumpet" + ] + } +} \ No newline at end of file diff --git a/test-app/src/main/ml-data/musicians/permissions.properties b/test-app/src/main/ml-data/musicians/permissions.properties new file mode 100644 index 0000000..a181f8b --- /dev/null +++ b/test-app/src/main/ml-data/musicians/permissions.properties @@ -0,0 +1 @@ +*=python-tester,read,python-tester,update diff --git a/test-app/src/main/ml-schemas/tde/musician.json b/test-app/src/main/ml-schemas/tde/musician.json new file mode 100644 index 0000000..7d0e175 --- /dev/null +++ b/test-app/src/main/ml-schemas/tde/musician.json @@ -0,0 +1,28 @@ +{ + "template": { + "context": "/musician", + "rows": [ + { + "schemaName": "test", + "viewName": "musician", + "columns": [ + { + "name": "lastName", + "scalarType": "string", + "val": "lastName" + }, + { + "name": "firstName", + "scalarType": "string", + "val": "firstName" + }, + { + "name": "dob", + "scalarType": "date", + "val": "dob" + } + ] + } + ] + } + } \ No newline at end of file diff --git a/tests/test_graphql.py b/tests/test_graphql.py new file mode 100644 index 0000000..54e2fd4 --- /dev/null +++ b/tests/test_graphql.py @@ -0,0 +1,25 @@ +def test_graphql(client): + data = client.rows.graphql("query musicianQuery { test_musician { lastName firstName dob } }") + musicians = data["data"]["test_musician"] + assert 4 == len(musicians) + assert 1 == len([m for m in musicians if m["lastName"] == "Armstrong"]) + + +def test_graphql_return_response(client): + response = client.rows.graphql("query musicianQuery { test_musician { lastName firstName dob } }", return_response=True) + assert 200 == response.status_code + data = response.json() + musicians = data["data"]["test_musician"] + assert 4 == len(musicians) + assert 1 == len([m for m in musicians if m["lastName"] == "Armstrong"]) + + +def test_graphql_bad_graphql(client): + response = client.rows.graphql("query musicianQuery { test_musician { lastName firstName dob } ") + assert 1 == len(response['errors']) + assert 'GRAPHQL-PARSE: Error parsing the GraphQL request string => \nquery musicianQuery { test_musician { lastName firstName dob } ' == response['errors'][0]['message'] + + +def test_graphql_bad_user(not_rest_user_client): + response = not_rest_user_client.rows.graphql("query musicianQuery { test_musician { lastName firstName dob } }") + assert 403 == response.status_code diff --git a/tests/test_read_documents.py b/tests/test_read_documents.py index 08f80b8..eac6b65 100644 --- a/tests/test_read_documents.py +++ b/tests/test_read_documents.py @@ -91,8 +91,9 @@ def test_read_only_collections(client: Client): doc1 = docs[0] assert doc1.uri == "/doc1.json" - assert len(doc1.collections) == 1 - assert doc1.collections[0] == "test-data" + assert len(doc1.collections) == 2 + assert "test-data" in doc1.collections + assert "search-test" in doc1.collections assert doc1.content is None assert doc1.permissions is None assert doc1.quality is None @@ -101,8 +102,9 @@ def test_read_only_collections(client: Client): doc2 = docs[1] assert doc2.uri == "/doc2.xml" - assert len(doc2.collections) == 1 - assert doc2.collections[0] == "test-data" + assert len(doc2.collections) == 2 + assert "test-data" in doc1.collections + assert "search-test" in doc1.collections assert doc2.content is None assert doc2.permissions is None assert doc2.quality is None diff --git a/tests/test_search_docs.py b/tests/test_search_docs.py index ebaba69..d9a1603 100644 --- a/tests/test_search_docs.py +++ b/tests/test_search_docs.py @@ -75,17 +75,21 @@ def test_search_options(client: Client): def test_collection(client: Client): docs = client.documents.search( - categories=["content", "collections"], collections=["test-data"] + categories=["content", "collections"], collections=["search-test"] ) assert len(docs) == 2 doc1 = next(doc for doc in docs if doc.uri == "/doc1.json") assert doc1.content is not None - assert doc1.collections[0] == "test-data" + assert len(doc1.collections) == 2 + assert "test-data" in doc1.collections + assert "search-test" in doc1.collections doc2 = next(doc for doc in docs if doc.uri == "/doc2.xml") assert doc2.content is not None - assert doc2.collections[0] == "test-data" + assert len(doc1.collections) == 2 + assert "test-data" in doc1.collections + assert "search-test" in doc1.collections def test_not_rest_user(not_rest_user_client: Client):