Skip to content

Commit 9aae9a1

Browse files
committed
DEVEXP-589 Added eval docs
Tossed in a couple more test cases. Curiously could not find a way to return a "binary()" with it being associated with a URI.
1 parent 7c8d67b commit 9aae9a1

File tree

7 files changed

+161
-18
lines changed

7 files changed

+161
-18
lines changed

docs/eval.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
---
2+
layout: default
3+
title: Executing code
4+
nav_order: 5
5+
---
6+
7+
The [MarkLogic REST service extension](https://docs.marklogic.com/REST/client/service-extension) supports the
8+
execution of custom code, whether via an inline script or an existing module in your application's modules database.
9+
The MarkLogic Python client simplifies execution of custom code both by managing some of the complexity of submitting
10+
and custom code and also converting the multipart response into more useful Python data types.
11+
12+
## Setup
13+
14+
The examples below all depend on the instructions in the [setup guide](example-setup.md) having already been performed.
15+
16+
To try out the examples, start a Python shell and first run the following:
17+
18+
```
19+
from marklogic import Client
20+
client = Client('http://localhost:8000', digest=('python-user', 'pyth0n'))
21+
```
22+
23+
## Executing ad-hoc queries
24+
25+
The [v1/eval REST endpoint](https://docs.marklogic.com/REST/POST/v1/eval) supports the execution of ad-hoc JavaScript
26+
and XQuery queries. Each type of query can be easily submitted via the client:
27+
28+
```
29+
client.eval.javascript("fn.currentDateTime()")
30+
client.eval.xquery("fn:current-dateTime()")
31+
```
32+
33+
Variables can optionally be provided via a `dict`:
34+
35+
```
36+
results = client.eval.javascript('Sequence.from([{"hello": myValue}])', vars={"myValue": "world"})
37+
assert "world" == results[0]["hello"]
38+
```
39+
40+
Because the REST endpoint returns a sequence of items, the client will always return a list of values. See the section
41+
below on how data types are converted to understand how the client will convert each value into an appropriate Python
42+
data type.
43+
44+
## Invoking modules
45+
46+
The [v1/invoke REST endpoint](https://docs.marklogic.com/REST/POST/v1/invoke) supports the execution of JavaScript
47+
and XQuery main modules that have been deployed to your application's modules database. A module can be invoked via
48+
the client in the following manner:
49+
50+
```
51+
client.invoke("/path/to/module.sjs")
52+
```
53+
54+
55+
## Conversion of data types
56+
57+
The REST endpoints for evaluating ad-hoc code and for invoking a module both return a sequence of values, with each
58+
value having MarkLogic-specific type information. The client will use this type information to convert each value into
59+
an appropriate Python data type. For example, each JSON object into the example below is converted into a `dict`:
60+
61+
```
62+
results = client.eval.javascript('Sequence.from([{"doc": 1}, {"doc": 2}])')
63+
assert len(results) == 2
64+
assert results[0]["doc"] == 1
65+
assert results[1]["doc"] == 2
66+
```
67+
68+
The following table describes how each MarkLogic type is associated with a Python data type. For any
69+
MarkLogic type not listed in the table, the value is not converted and will be of type `bytes`.
70+
71+
| MarkLogic type | Python type |
72+
| --- | --- |
73+
| string | str |
74+
| integer | int |
75+
| boolean | bool |
76+
| decimal | [Decimal](https://docs.python.org/3/library/decimal.html) |
77+
| map | dict |
78+
| element() | str |
79+
| array | list |
80+
| array-node() | list |
81+
| object-node() | dict or marklogic.documents.Document |
82+
| document-node() | str or marklogic.documents.Document |
83+
| binary() | marklogic.documents.Document |
84+
85+
For the `object-node()` and `document-node()` entries in the above table, a `marklogic.documents.Document` instance
86+
will be returned if the MarkLogic value is associated with a URI via the multipart `X-URI` header. Otherwise, a `dict`
87+
or `str` is returned respectively.
88+
89+
## Returning the original HTTP response
90+
91+
Each `client.eval` method and `client.invoke` accept a `return_response` argument. When that
92+
argument is set to `True`, the original response is returned. This can be useful for custom
93+
processing of the response or debugging requests.

docs/example-setup.md

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,29 @@ nav_order: 2
55
permalink: /setup
66
---
77

8-
The examples in this documentation depend on a particular MarkLogic username and password. If you
9-
would like to try these examples out against your own installation of MarkLogic, you will need to create this
10-
MarkLogic user. To do so, please go to the Admin application for your MarkLogic instance - e.g. if you are running MarkLogic locally, this will be at <http://localhost:8001> - and authenticate as your "admin" user.
11-
Then perform the following steps to create a new user:
8+
The examples in this documentation depend on a particular MarkLogic user with a role containing specific privileges.
9+
If you would like to try these examples out against your own installation of MarkLogic, you will need to create this
10+
MarkLogic user and role. To do so, please go to the Admin application for your MarkLogic instance - e.g. if you are
11+
running MarkLogic locally, this will be at <http://localhost:8001> - and authenticate as your "admin" user.
12+
Then perform the following steps to create a new role:
13+
14+
1. Click on "Roles" in the "Security" box.
15+
2. Click on "Create".
16+
3. In the form, enter "python-docs-role" for "Role Name".
17+
4. Scroll down and select the "rest-extension-user", "rest-reader", "rest-writer", and "tde-admin" roles.
18+
5. Scroll further down and select the "xdbc:eval", "xdbc:invoke", and "xdmp:eval-in" privileges.
19+
6. Scroll to the top or bottom and click on "OK" to create the role.
20+
21+
After creating the role, return to the Admin application home page and perform the following steps:
1222

1323
1. Click on "Users" in the "Security" box.
1424
2. Click on "Create".
1525
3. In the form, enter "python-user" for "User Name" and "pyth0n" as the password.
16-
4. Scroll down until you see the "Roles" section. Click on the "rest-reader", "rest-writer", and "security" checkboxes.
26+
4. Scroll down until you see the "Roles" section and select the "python-docs-role" role.
1727
5. Scroll to the top or bottom and click on "OK" to create the user.
1828

19-
(The `security` role is only needed to allow for the user to load documents into the out-of-the-box Schemas database
20-
in MarkLogic; in a production application, an admin or admin-like user would typically be used for this use case.)
29+
(Note that you could use the `admin` role instead to grant full access to all features in MarkLogic, but this is
30+
generally discouraged for security reasons.)
2131

2232
You can verify that you correctly created the user by accessing the REST API for the out-of-the-box REST API
2333
server in MarkLogic that listens on port 8000. Go to <http://localhost:8000/v1/search> (changing "localhost" to

docs/transactions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
layout: default
33
title: Managing transactions
4-
nav_order: 5
4+
nav_order: 6
55
---
66

77
The [MarkLogic REST transactions service](https://docs.marklogic.com/REST/client/transaction-management)

marklogic/client.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -112,16 +112,14 @@ def process_multipart_mixed_response(self, response):
112112
transformed_parts = []
113113
for part in parts:
114114
encoding = part.encoding
115-
primitive_header = part.headers["X-Primitive".encode(encoding)].decode(
116-
encoding
117-
)
118-
primitive_function = Client.__primitive_value_converters.get(
119-
primitive_header
120-
)
115+
header = part.headers["X-Primitive".encode(encoding)].decode(encoding)
116+
primitive_function = Client.__primitive_value_converters.get(header)
121117
if primitive_function is not None:
122118
transformed_parts.append(primitive_function(part))
123119
else:
124-
transformed_parts.append(part.text)
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)
125123
return transformed_parts
126124

127125
@property
@@ -159,6 +157,9 @@ def eval(self):
159157
"array-node()": lambda part: json.loads(part.text),
160158
"object-node()": lambda part: Client.__process_object_node_part(part),
161159
"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.
162163
"binary()": lambda part: Document(
163164
Client.__get_decoded_uri_from_part(part), part.content
164165
),
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"role-name": "python-docs-role",
3+
"description": "Used by the documentation, not by tests",
4+
"role": [
5+
"rest-extension-user",
6+
"rest-reader",
7+
"rest-writer",
8+
"tde-admin"
9+
],
10+
"privilege": [
11+
{
12+
"privilege-name": "xdbc:eval",
13+
"action": "http://marklogic.com/xdmp/privileges/xdbc-eval",
14+
"kind": "execute"
15+
},
16+
{
17+
"privilege-name": "xdbc:invoke",
18+
"action": "http://marklogic.com/xdmp/privileges/xdbc-invoke",
19+
"kind": "execute"
20+
},
21+
{
22+
"privilege-name": "xdmp:eval-in",
23+
"action": "http://marklogic.com/xdmp/privileges/xdmp-eval-in",
24+
"kind": "execute"
25+
}
26+
]
27+
}

test-app/src/main/ml-config/security/users/python-docs-user.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
"password": "pyth0n",
44
"description": "Used by the documentation, not by tests",
55
"role": [
6-
"rest-reader",
7-
"rest-writer",
8-
"security"
6+
"python-docs-role"
97
]
108
}

tests/test_eval.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,20 @@ def test_javascript_script(client):
108108
assert [[]] == parts
109109

110110

111+
def test_base64Binary(client):
112+
parts = client.eval.xquery('xs:base64Binary(doc("/musicians/logo.png"))')
113+
assert len(parts) == 1
114+
assert type(parts[0]) is bytes
115+
116+
117+
def test_hexBinary(client):
118+
# No idea what this value is, found it in a DHF test.
119+
b = "3f3c6d78206c657673726f693d6e3122302e20226e656f636964676e223d54552d4622383e3f"
120+
parts = client.eval.xquery(f"xs:hexBinary('{b}')")
121+
assert len(parts) == 1
122+
assert type(parts[0]) is bytes
123+
124+
111125
def __verify_common_primitives(parts):
112126
assert type(parts[0]) is str
113127
assert "A" == parts[0]

0 commit comments

Comments
 (0)