Skip to content

Commit 43a302b

Browse files
authored
Merge pull request #44 from marklogic/feature/refactor-eval
Moved eval guts into impl/eval.py
2 parents bf9b42c + b430f12 commit 43a302b

File tree

4 files changed

+166
-186
lines changed

4 files changed

+166
-186
lines changed

marklogic/client.py

Lines changed: 59 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import json
22
import requests
33

4-
from decimal import Decimal
54
from marklogic.cloud_auth import MarkLogicCloudAuth
6-
from marklogic.documents import Document, DocumentManager
7-
from marklogic.eval import EvalManager
5+
from marklogic.documents import DocumentManager
6+
from marklogic.impl.eval import process_multipart_mixed_response
87
from marklogic.rows import RowManager
98
from marklogic.transactions import TransactionManager
109
from requests.auth import HTTPDigestAuth
11-
from requests_toolbelt.multipart.decoder import MultipartDecoder
1210
from urllib.parse import urljoin
1311

1412

@@ -72,6 +70,61 @@ def prepare_request(self, request, *args, **kwargs):
7270
request.url = urljoin(self.base_url, request.url)
7371
return super(Client, self).prepare_request(request, *args, **kwargs)
7472

73+
@property
74+
def documents(self):
75+
if not hasattr(self, "_documents"):
76+
self._documents = DocumentManager(session=self)
77+
return self._documents
78+
79+
@property
80+
def rows(self):
81+
if not hasattr(self, "_rows"):
82+
self._rows = RowManager(session=self)
83+
return self._rows
84+
85+
@property
86+
def transactions(self):
87+
if not hasattr(self, "_transactions"):
88+
self._transactions = TransactionManager(session=self)
89+
return self._transactions
90+
91+
def eval(
92+
self,
93+
javascript: str = None,
94+
xquery: str = None,
95+
vars: dict = None,
96+
return_response: bool = False,
97+
**kwargs,
98+
):
99+
"""
100+
Send a script to MarkLogic via a POST to the endpoint
101+
defined at https://docs.marklogic.com/REST/POST/v1/eval. Must define either
102+
'javascript' or 'xquery'.
103+
104+
:param javascript: a JavaScript script
105+
:param xquery: an XQuery script
106+
:param vars: a dict containing variables to include
107+
:param return_response: boolean specifying if the entire original response
108+
object should be returned (True) or if only the data should be returned (False)
109+
upon a success (2xx) response. Note that if the status code of the response is
110+
not 2xx, then the entire response is always returned.
111+
"""
112+
data = {}
113+
if javascript:
114+
data = {"javascript": javascript}
115+
elif xquery:
116+
data = {"xquery": xquery}
117+
else:
118+
raise ValueError("Must define either 'javascript' or 'xquery' argument.")
119+
if vars:
120+
data["vars"] = json.dumps(vars)
121+
response = self.post("v1/eval", data=data, **kwargs)
122+
return (
123+
process_multipart_mixed_response(response)
124+
if response.status_code == 200 and not return_response
125+
else response
126+
)
127+
75128
def invoke(
76129
self, module: str, vars: dict = None, return_response: bool = False, **kwargs
77130
):
@@ -88,97 +141,11 @@ def invoke(
88141
not 2xx, then the entire response is always returned.
89142
"""
90143
data = {"module": module}
91-
if vars is not None:
144+
if vars:
92145
data["vars"] = json.dumps(vars)
93146
response = self.post("v1/invoke", data=data, **kwargs)
94147
return (
95-
self.process_multipart_mixed_response(response)
148+
process_multipart_mixed_response(response)
96149
if response.status_code == 200 and not return_response
97150
else response
98151
)
99-
100-
def process_multipart_mixed_response(self, response):
101-
"""
102-
Process a multipart REST response by putting them in a list and
103-
transforming each part based on the "X-Primitive" header.
104-
105-
:param response: The original multipart/mixed response from a call to a
106-
MarkLogic server.
107-
"""
108-
if "Content-Length" in response.headers:
109-
return None
110-
111-
parts = MultipartDecoder.from_response(response).parts
112-
transformed_parts = []
113-
for part in parts:
114-
encoding = part.encoding
115-
header = part.headers["X-Primitive".encode(encoding)].decode(encoding)
116-
primitive_function = Client.__primitive_value_converters.get(header)
117-
if primitive_function is not None:
118-
transformed_parts.append(primitive_function(part))
119-
else:
120-
# Return the binary created by requests_toolbelt so we don't get an
121-
# error trying to convert it to something else.
122-
transformed_parts.append(part.content)
123-
return transformed_parts
124-
125-
@property
126-
def documents(self):
127-
if not hasattr(self, "_documents"):
128-
self._documents = DocumentManager(session=self)
129-
return self._documents
130-
131-
@property
132-
def rows(self):
133-
if not hasattr(self, "_rows"):
134-
self._rows = RowManager(session=self)
135-
return self._rows
136-
137-
@property
138-
def transactions(self):
139-
if not hasattr(self, "_transactions"):
140-
self._transactions = TransactionManager(session=self)
141-
return self._transactions
142-
143-
@property
144-
def eval(self):
145-
if not hasattr(self, "_eval"):
146-
self._eval = EvalManager(session=self)
147-
return self._eval
148-
149-
__primitive_value_converters = {
150-
"integer": lambda part: int(part.text),
151-
"decimal": lambda part: Decimal(part.text),
152-
"boolean": lambda part: ("False" == part.text),
153-
"string": lambda part: part.text,
154-
"map": lambda part: json.loads(part.text),
155-
"element()": lambda part: part.text,
156-
"array": lambda part: json.loads(part.text),
157-
"array-node()": lambda part: json.loads(part.text),
158-
"object-node()": lambda part: Client.__process_object_node_part(part),
159-
"document-node()": lambda part: Client.__process_document_node_part(part),
160-
# It appears that binary() will only be returned for a binary node retrieved
161-
# from the database, and thus an X-URI will always exist. Have not found a
162-
# scenario that indicates otherwise.
163-
"binary()": lambda part: Document(
164-
Client.__get_decoded_uri_from_part(part), part.content
165-
),
166-
}
167-
168-
def __get_decoded_uri_from_part(part):
169-
encoding = part.encoding
170-
return part.headers["X-URI".encode(encoding)].decode(encoding)
171-
172-
def __process_object_node_part(part):
173-
if b"X-URI" in part.headers:
174-
return Document(
175-
Client.__get_decoded_uri_from_part(part), json.loads(part.text)
176-
)
177-
else:
178-
return json.loads(part.text)
179-
180-
def __process_document_node_part(part):
181-
if b"X-URI" in part.headers:
182-
return Document(Client.__get_decoded_uri_from_part(part), part.text)
183-
else:
184-
return part.text

marklogic/eval.py

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

marklogic/impl/eval.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import json
2+
3+
from decimal import Decimal
4+
from marklogic.documents import Document
5+
from requests import Response
6+
from requests_toolbelt.multipart.decoder import MultipartDecoder
7+
8+
"""
9+
Supports working with data returned by the v1/eval and v1/invoke endpoints.
10+
"""
11+
12+
__primitive_value_converters = {
13+
"integer": lambda part: int(part.text),
14+
"decimal": lambda part: Decimal(part.text),
15+
"boolean": lambda part: ("False" == part.text),
16+
"string": lambda part: part.text,
17+
"map": lambda part: json.loads(part.text),
18+
"element()": lambda part: part.text,
19+
"array": lambda part: json.loads(part.text),
20+
"array-node()": lambda part: json.loads(part.text),
21+
"object-node()": lambda part: __process_object_node_part(part),
22+
"document-node()": lambda part: __process_document_node_part(part),
23+
# It appears that binary() will only be returned for a binary node retrieved
24+
# from the database, and thus an X-URI will always exist. Have not found a
25+
# scenario that indicates otherwise.
26+
"binary()": lambda part: Document(__get_decoded_uri_from_part(part), part.content),
27+
}
28+
29+
30+
def process_multipart_mixed_response(response: Response) -> list:
31+
"""
32+
Process a multipart REST response by putting them in a list and
33+
transforming each part based on the "X-Primitive" header.
34+
35+
:param response: The original multipart/mixed response from a call to a
36+
MarkLogic server.
37+
"""
38+
39+
# The presence of this header indicates that the call returned an empty sequence.
40+
if "Content-Length" in response.headers:
41+
return []
42+
43+
parts = MultipartDecoder.from_response(response).parts
44+
transformed_parts = []
45+
for part in parts:
46+
encoding = part.encoding
47+
header = part.headers["X-Primitive".encode(encoding)].decode(encoding)
48+
primitive_function = __primitive_value_converters.get(header)
49+
if primitive_function is not None:
50+
transformed_parts.append(primitive_function(part))
51+
else:
52+
# Return the binary created by requests_toolbelt so we don't get an
53+
# error trying to convert it to something else.
54+
transformed_parts.append(part.content)
55+
return transformed_parts
56+
57+
58+
def __get_decoded_uri_from_part(part):
59+
encoding = part.encoding
60+
return part.headers["X-URI".encode(encoding)].decode(encoding)
61+
62+
63+
def __process_object_node_part(part):
64+
if b"X-URI" in part.headers:
65+
return Document(__get_decoded_uri_from_part(part), json.loads(part.text))
66+
else:
67+
return json.loads(part.text)
68+
69+
70+
def __process_document_node_part(part):
71+
if b"X-URI" in part.headers:
72+
return Document(__get_decoded_uri_from_part(part), part.text)
73+
else:
74+
return part.text

0 commit comments

Comments
 (0)