Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
151 changes: 59 additions & 92 deletions marklogic/client.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import json
import requests

from decimal import Decimal
from marklogic.cloud_auth import MarkLogicCloudAuth
from marklogic.documents import Document, DocumentManager
from marklogic.eval import EvalManager
from marklogic.documents import DocumentManager
from marklogic.impl.eval import process_multipart_mixed_response
from marklogic.rows import RowManager
from marklogic.transactions import TransactionManager
from requests.auth import HTTPDigestAuth
from requests_toolbelt.multipart.decoder import MultipartDecoder
from urllib.parse import urljoin


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

@property
def documents(self):
if not hasattr(self, "_documents"):
self._documents = DocumentManager(session=self)
return self._documents

@property
def rows(self):
if not hasattr(self, "_rows"):
self._rows = RowManager(session=self)
return self._rows

@property
def transactions(self):
if not hasattr(self, "_transactions"):
self._transactions = TransactionManager(session=self)
return self._transactions

def eval(
self,
javascript: str = None,
xquery: str = None,
vars: dict = None,
return_response: bool = False,
**kwargs,
):
"""
Send a script to MarkLogic via a POST to the endpoint
defined at https://docs.marklogic.com/REST/POST/v1/eval. Must define either
'javascript' or 'xquery'.

:param javascript: a JavaScript script
:param xquery: an XQuery script
:param vars: a dict containing variables to include
: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.
"""
data = {}
if javascript:
data = {"javascript": javascript}
elif xquery:
data = {"xquery": xquery}
else:
raise ValueError("Must define either 'javascript' or 'xquery' argument.")
if vars:
data["vars"] = json.dumps(vars)
response = self.post("v1/eval", data=data, **kwargs)
return (
process_multipart_mixed_response(response)
if response.status_code == 200 and not return_response
else response
)

def invoke(
self, module: str, vars: dict = None, return_response: bool = False, **kwargs
):
Expand All @@ -88,97 +141,11 @@ def invoke(
not 2xx, then the entire response is always returned.
"""
data = {"module": module}
if vars is not None:
if vars:
data["vars"] = json.dumps(vars)
response = self.post("v1/invoke", data=data, **kwargs)
return (
self.process_multipart_mixed_response(response)
process_multipart_mixed_response(response)
if response.status_code == 200 and not return_response
else response
)

def process_multipart_mixed_response(self, response):
"""
Process a multipart REST response by putting them in a list and
transforming each part based on the "X-Primitive" header.

:param response: The original multipart/mixed response from a call to a
MarkLogic server.
"""
if "Content-Length" in response.headers:
return None

parts = MultipartDecoder.from_response(response).parts
transformed_parts = []
for part in parts:
encoding = part.encoding
header = part.headers["X-Primitive".encode(encoding)].decode(encoding)
primitive_function = Client.__primitive_value_converters.get(header)
if primitive_function is not None:
transformed_parts.append(primitive_function(part))
else:
# Return the binary created by requests_toolbelt so we don't get an
# error trying to convert it to something else.
transformed_parts.append(part.content)
return transformed_parts

@property
def documents(self):
if not hasattr(self, "_documents"):
self._documents = DocumentManager(session=self)
return self._documents

@property
def rows(self):
if not hasattr(self, "_rows"):
self._rows = RowManager(session=self)
return self._rows

@property
def transactions(self):
if not hasattr(self, "_transactions"):
self._transactions = TransactionManager(session=self)
return self._transactions

@property
def eval(self):
if not hasattr(self, "_eval"):
self._eval = EvalManager(session=self)
return self._eval

__primitive_value_converters = {
"integer": lambda part: int(part.text),
"decimal": lambda part: Decimal(part.text),
"boolean": lambda part: ("False" == part.text),
"string": lambda part: part.text,
"map": lambda part: json.loads(part.text),
"element()": lambda part: part.text,
"array": lambda part: json.loads(part.text),
"array-node()": lambda part: json.loads(part.text),
"object-node()": lambda part: Client.__process_object_node_part(part),
"document-node()": lambda part: Client.__process_document_node_part(part),
# It appears that binary() will only be returned for a binary node retrieved
# from the database, and thus an X-URI will always exist. Have not found a
# scenario that indicates otherwise.
"binary()": lambda part: Document(
Client.__get_decoded_uri_from_part(part), part.content
),
}

def __get_decoded_uri_from_part(part):
encoding = part.encoding
return part.headers["X-URI".encode(encoding)].decode(encoding)

def __process_object_node_part(part):
if b"X-URI" in part.headers:
return Document(
Client.__get_decoded_uri_from_part(part), json.loads(part.text)
)
else:
return json.loads(part.text)

def __process_document_node_part(part):
if b"X-URI" in part.headers:
return Document(Client.__get_decoded_uri_from_part(part), part.text)
else:
return part.text
73 changes: 0 additions & 73 deletions marklogic/eval.py

This file was deleted.

74 changes: 74 additions & 0 deletions marklogic/impl/eval.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import json

from decimal import Decimal
from marklogic.documents import Document
from requests import Response
from requests_toolbelt.multipart.decoder import MultipartDecoder

"""
Supports working with data returned by the v1/eval and v1/invoke endpoints.
"""

__primitive_value_converters = {
"integer": lambda part: int(part.text),
"decimal": lambda part: Decimal(part.text),
"boolean": lambda part: ("False" == part.text),
"string": lambda part: part.text,
"map": lambda part: json.loads(part.text),
"element()": lambda part: part.text,
"array": lambda part: json.loads(part.text),
"array-node()": lambda part: json.loads(part.text),
"object-node()": lambda part: __process_object_node_part(part),
"document-node()": lambda part: __process_document_node_part(part),
# It appears that binary() will only be returned for a binary node retrieved
# from the database, and thus an X-URI will always exist. Have not found a
# scenario that indicates otherwise.
"binary()": lambda part: Document(__get_decoded_uri_from_part(part), part.content),
}


def process_multipart_mixed_response(response: Response) -> list:
"""
Process a multipart REST response by putting them in a list and
transforming each part based on the "X-Primitive" header.

:param response: The original multipart/mixed response from a call to a
MarkLogic server.
"""

# The presence of this header indicates that the call returned an empty sequence.
if "Content-Length" in response.headers:
return []

parts = MultipartDecoder.from_response(response).parts
transformed_parts = []
for part in parts:
encoding = part.encoding
header = part.headers["X-Primitive".encode(encoding)].decode(encoding)
primitive_function = __primitive_value_converters.get(header)
if primitive_function is not None:
transformed_parts.append(primitive_function(part))
else:
# Return the binary created by requests_toolbelt so we don't get an
# error trying to convert it to something else.
transformed_parts.append(part.content)
return transformed_parts


def __get_decoded_uri_from_part(part):
encoding = part.encoding
return part.headers["X-URI".encode(encoding)].decode(encoding)


def __process_object_node_part(part):
if b"X-URI" in part.headers:
return Document(__get_decoded_uri_from_part(part), json.loads(part.text))
else:
return json.loads(part.text)


def __process_document_node_part(part):
if b"X-URI" in part.headers:
return Document(__get_decoded_uri_from_part(part), part.text)
else:
return part.text
Loading