Skip to content

Commit 216ea24

Browse files
authored
Merge pull request #32 from BillFarber/feature/submitOptic
DEVEXP-574: Submit Optic plan with Python client
2 parents c87e3c5 + 8ff4477 commit 216ea24

File tree

5 files changed

+188
-7
lines changed

5 files changed

+188
-7
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ __pycache__
55
.venv
66
venv
77
.idea
8+
9+
.DS_Store

marklogic/rows.py

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import json
2-
from requests import Session
2+
from requests import Session, Response
33

44
"""
55
Defines a RowManager class to simplify usage of the "/v1/rows" & "/v1/rows/graphql" REST
@@ -11,10 +11,11 @@ class RowManager:
1111
"""
1212
Provides a method to simplify sending a GraphQL request to the GraphQL rows endpoint.
1313
"""
14+
1415
def __init__(self, session: Session):
1516
self._session = session
16-
17-
def graphql(self, graphql_query, return_response=False, *args, **kwargs):
17+
18+
def graphql(self, graphql_query: str, return_response: bool = False, *args, **kwargs):
1819
"""
1920
Send a GraphQL query to MarkLogic via a POST to the endpoint defined at
2021
https://docs.marklogic.com/REST/POST/v1/rows/graphql
@@ -40,4 +41,92 @@ def graphql(self, graphql_query, return_response=False, *args, **kwargs):
4041
response.json()
4142
if response.status_code == 200 and not return_response
4243
else response
43-
)
44+
)
45+
46+
__accept_switch = {
47+
"json": "application/json",
48+
"xml": "application/xml",
49+
"csv": "text/csv",
50+
"json-seq": "application/json-seq",
51+
"mixed": "application/xml, multipart/mixed"
52+
}
53+
54+
__query_format_switch = {
55+
"json": lambda response: response.json(),
56+
"xml": lambda response: response.text,
57+
"csv": lambda response: response.text,
58+
"json-seq": lambda response: response.text,
59+
"mixed": lambda response: response
60+
}
61+
62+
def query(self, dsl: str = None, plan: dict = None, sql: str = None, sparql: str = None, format: str = "json", return_response: bool = False, *args, **kwargs):
63+
"""
64+
Send a query to MarkLogic via a POST to the endpoint defined at
65+
https://docs.marklogic.com/REST/POST/v1/rows
66+
Just like that endpoint, this function can be used for four different types of
67+
queries: Optic DSL, Serialized Optic, SQL, and SPARQL. The type of query
68+
processed by the function is dependent upon the parameter used in the call to
69+
the function.
70+
For more information about Optic and using the Optic DSL, SQL, and SPARQL,
71+
see https://docs.marklogic.com/guide/app-dev/OpticAPI
72+
If multiple query parameters are passed into the call, the function uses the
73+
query parameter that is first in the list: dsl, plan, sql, sparql.
74+
75+
:param dsl: an Optic DSL query
76+
:param plan: a serialized Optic query
77+
:param sql: an SQL query
78+
:param sparql: a SPARQL query
79+
:param return_response: boolean specifying if the entire original response
80+
object should be returned (True) or if only the data should be returned (False)
81+
upon a success (2xx) response. Note that if the status code of the response is
82+
not 2xx, then the entire response is always returned.
83+
"""
84+
request_info = self.__get_request_info(dsl, plan, sql, sparql)
85+
headers = kwargs.pop("headers", {})
86+
headers["Content-Type"] = request_info["content-type"]
87+
headers["Accept"] = RowManager.__accept_switch.get(format)
88+
response = self._session.post(
89+
"v1/rows",
90+
headers=headers,
91+
data=request_info["data"],
92+
**kwargs
93+
)
94+
return (
95+
RowManager.__query_format_switch.get(format)(response)
96+
if response.status_code == 200 and not return_response
97+
else response
98+
)
99+
100+
def __get_request_info(self, dsl: str, plan: dict, sql: str, sparql: str):
101+
"""
102+
Examine the parameters passed into the query function to determine what value
103+
should be passed to the endpoint and what the content-type header should be.
104+
105+
:param dsl: an Optic DSL query
106+
:param plan: a serialized Optic query
107+
:param sql: an SQL query
108+
:param sparql: a SPARQL query
109+
dict object returned contains the two values required to make the POST request.
110+
"""
111+
if dsl is not None:
112+
return {
113+
"content-type": "application/vnd.marklogic.querydsl+javascript",
114+
"data": dsl
115+
}
116+
if plan is not None:
117+
return {
118+
"content-type": "application/json",
119+
"data": plan
120+
}
121+
if sql is not None:
122+
return {
123+
"content-type": "application/sql",
124+
"data": sql
125+
}
126+
if sparql is not None:
127+
return {
128+
"content-type": "application/sparql-query",
129+
"data": sparql
130+
}
131+
else:
132+
raise ValueError("No query found; must specify one of: dsl, plan, sql, or sparql")

test-app/src/main/ml-config/databases/modules-database.json

Lines changed: 0 additions & 3 deletions
This file was deleted.

tests/test_query.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from pytest import raises
2+
3+
dsl_query = 'op.fromView("test","musician").orderBy(op.col("lastName"))'
4+
serialized_query = '{"$optic":{"ns":"op", "fn":"operators", "args":[{"ns":"op", "fn":"from-view", "args":["test", "musician"]}, {"ns":"op", "fn":"order-by", "args":[{"ns":"op", "fn":"col", "args":["lastName"]}]}]}}'
5+
sql_query = 'select * from musician order by lastName'
6+
sparql_query = 'PREFIX musician: <http://marklogic.com/column/test/musician/> SELECT * WHERE {?s musician:lastName ?lastName} ORDER BY ?lastName'
7+
8+
9+
def test_dsl_default(client):
10+
data = client.rows.query(dsl_query)
11+
verify_four_musicians_are_returned_in_json(data, "test.musician.lastName")
12+
13+
14+
def test_dsl_default_return_response(client):
15+
response = client.rows.query(dsl_query, return_response=True)
16+
assert 200 == response.status_code
17+
verify_four_musicians_are_returned_in_json(response.json(), "test.musician.lastName")
18+
19+
20+
def test_query_bad_user(not_rest_user_client):
21+
response = not_rest_user_client.rows.query(dsl_query)
22+
assert 403 == response.status_code
23+
24+
25+
def test_dsl_json(client):
26+
data = client.rows.query(dsl_query, format="json")
27+
verify_four_musicians_are_returned_in_json(data, "test.musician.lastName")
28+
29+
30+
def test_dsl_xml(client):
31+
data = client.rows.query(dsl_query, format="xml")
32+
verify_four_musicians_are_returned_in_xml_string(data)
33+
34+
def test_dsl_csv(client):
35+
data = client.rows.query(dsl_query, format="csv")
36+
verify_four_musicians_are_returned_in_csv(data)
37+
38+
def test_dsl_json_seq(client):
39+
data = client.rows.query(dsl_query, format="json-seq")
40+
verify_four_musicians_are_returned_in_json_seq(data)
41+
42+
def test_dsl_mixed(client):
43+
response = client.rows.query(dsl_query, format="mixed")
44+
verify_four_musicians_are_returned_in_json(response.json(), "test.musician.lastName")
45+
46+
47+
def test_serialized_default(client):
48+
data = client.rows.query(plan=serialized_query)
49+
verify_four_musicians_are_returned_in_json(data, "test.musician.lastName")
50+
51+
52+
def test_sql_default(client):
53+
data = client.rows.query(sql=sql_query)
54+
verify_four_musicians_are_returned_in_json(data, "test.musician.lastName")
55+
56+
57+
def test_sparql_default(client):
58+
data = client.rows.query(sparql=sparql_query)
59+
verify_four_musicians_are_returned_in_json(data, "lastName")
60+
61+
62+
def test_no_query_parameter_provided(client):
63+
with raises(ValueError, match="No query found; must specify one of: dsl, plan, sql, or sparql"):
64+
client.rows.query()
65+
66+
67+
def verify_four_musicians_are_returned_in_json(data, column_name):
68+
assert type(data) is dict
69+
assert 4 == len(data["rows"])
70+
for index, musician in enumerate(["Armstrong", "Byron", "Coltrane", "Davis"]):
71+
assert {'type': 'xs:string', 'value': musician} == data["rows"][index][column_name]
72+
73+
74+
def verify_four_musicians_are_returned_in_xml_string(data):
75+
assert type(data) is str
76+
assert 4 == data.count('lastName" type="xs:string">')
77+
for musician in ["Armstrong", "Byron", "Coltrane", "Davis"]:
78+
assert 'lastName" type="xs:string">' + musician in data
79+
80+
81+
def verify_four_musicians_are_returned_in_csv(data):
82+
assert type(data) is str
83+
assert 5 == len(data.split("\n"))
84+
for musician in ['Armstrong,Louis,1901-08-04', 'Byron,Don,1958-11-08', 'Coltrane,John,1926-09-23', 'Davis,Miles,1926-05-26']:
85+
assert musician in data
86+
87+
88+
def verify_four_musicians_are_returned_in_json_seq(data):
89+
assert type(data) is str
90+
rows = data.split("\n")
91+
assert 6 == len(rows)
92+
for musician in ["Armstrong", "Byron", "Coltrane", "Davis"]:
93+
assert 'lastName":{"type":"xs:string","value":"' + musician in data

0 commit comments

Comments
 (0)