Skip to content

Commit 7a1c408

Browse files
authored
Merge pull request #49 from marklogic/feature/eval-tx
Added transaction support for eval, invoke, and rows
2 parents a00574f + 1793398 commit 7a1c408

File tree

8 files changed

+88
-6
lines changed

8 files changed

+88
-6
lines changed

CONTRIBUTING.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ To run an individual test method:
3535

3636
Note that due to the pytest config in the `pyproject.toml` file, all
3737
[Python logging](https://docs.python.org/3/howto/logging.html) should appear immediately
38-
as tests are executed.
38+
as tests are executed. If you are using VSCode, you can see the logging by selecting
39+
"Python Test Log" in the "Output" panel.
3940

4041
## Testing the client in a Python shell
4142

marklogic/client.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from marklogic.documents import DocumentManager
66
from marklogic.internal.eval import process_multipart_mixed_response
77
from marklogic.rows import RowManager
8-
from marklogic.transactions import TransactionManager
8+
from marklogic.transactions import TransactionManager, Transaction
99
from requests.auth import HTTPDigestAuth
1010
from urllib.parse import urljoin
1111

@@ -93,6 +93,7 @@ def eval(
9393
javascript: str = None,
9494
xquery: str = None,
9595
vars: dict = None,
96+
tx: Transaction = None,
9697
return_response: bool = False,
9798
**kwargs,
9899
):
@@ -104,6 +105,7 @@ def eval(
104105
:param javascript: a JavaScript script
105106
:param xquery: an XQuery script
106107
:param vars: a dict containing variables to include
108+
:param tx: optional REST transaction in which to service this request.
107109
:param return_response: boolean specifying if the entire original response
108110
object should be returned (True) or if only the data should be returned (False)
109111
upon a success (2xx) response. Note that if the status code of the response is
@@ -118,15 +120,23 @@ def eval(
118120
raise ValueError("Must define either 'javascript' or 'xquery' argument.")
119121
if vars:
120122
data["vars"] = json.dumps(vars)
121-
response = self.post("v1/eval", data=data, **kwargs)
123+
params = kwargs.pop("params", {})
124+
if tx:
125+
params["txid"] = tx.id
126+
response = self.post("v1/eval", data=data, params=params, **kwargs)
122127
return (
123128
process_multipart_mixed_response(response)
124129
if response.status_code == 200 and not return_response
125130
else response
126131
)
127132

128133
def invoke(
129-
self, module: str, vars: dict = None, return_response: bool = False, **kwargs
134+
self,
135+
module: str,
136+
vars: dict = None,
137+
tx: Transaction = None,
138+
return_response: bool = False,
139+
**kwargs,
130140
):
131141
"""
132142
Send a script (XQuery or JavaScript) and possibly a dict of vars
@@ -135,6 +145,7 @@ def invoke(
135145
136146
:param module: The URI of a module in the modules database of the app server
137147
:param vars: a dict containing variables to include
148+
:param tx: optional REST transaction in which to service this request.
138149
:param return_response: boolean specifying if the entire original response
139150
object should be returned (True) or if only the data should be returned (False)
140151
upon a success (2xx) response. Note that if the status code of the response is
@@ -143,7 +154,10 @@ def invoke(
143154
data = {"module": module}
144155
if vars:
145156
data["vars"] = json.dumps(vars)
146-
response = self.post("v1/invoke", data=data, **kwargs)
157+
params = kwargs.pop("params", {})
158+
if tx:
159+
params["txid"] = tx.id
160+
response = self.post("v1/invoke", data=data, params=params, **kwargs)
147161
return (
148162
process_multipart_mixed_response(response)
149163
if response.status_code == 200 and not return_response

marklogic/rows.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
from requests import Session
3+
from marklogic.transactions import Transaction
34
from marklogic.internal.util import response_has_no_content
45

56

@@ -35,6 +36,7 @@ def query(
3536
sparql: str = None,
3637
graphql: str = None,
3738
format: str = "json",
39+
tx: Transaction = None,
3840
return_response: bool = False,
3941
**kwargs,
4042
):
@@ -58,6 +60,7 @@ def query(
5860
:param format: defines the format of the response. Valid values are "json",
5961
"xml", "csv", and "json-seq". If a GraphQL query is submitted, this parameter
6062
is ignored and a JSON response is always returned.
63+
:param tx: optional REST transaction in which to service this request.
6164
:param return_response: boolean specifying if the entire original response
6265
object should be returned (True) or if only the data should be returned (False)
6366
upon a success (2xx) response. Note that if the status code of the response is
@@ -82,7 +85,13 @@ def query(
8285
else:
8386
headers["Accept"] = value
8487

85-
response = self._session.post(path, headers=headers, data=data, **kwargs)
88+
params = kwargs.pop("params", {})
89+
if tx:
90+
params["txid"] = tx.id
91+
92+
response = self._session.post(
93+
path, headers=headers, data=data, params=params, **kwargs
94+
)
8695
if response.ok and not return_response:
8796
if response_has_no_content(response):
8897
return []
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
cts.doc("/doc1.json")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fn:doc("/doc2.xml")

tests/test_eval.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,18 @@ def test_no_script(client):
140140
client.eval(vars={})
141141

142142

143+
def test_transaction(client):
144+
locks = None
145+
with client.transactions.create() as tx:
146+
client.eval(javascript="cts.doc('/doc1.json')", tx=tx)
147+
client.eval(javascript="cts.doc('/doc2.xml')", tx=tx)
148+
locks = client.eval(javascript="xdmp.transactionLocks()", tx=tx)
149+
150+
read_locks = locks[0]["read"]
151+
assert "/doc1.json" in read_locks
152+
assert "/doc2.xml" in read_locks
153+
154+
143155
def __verify_common_primitives(parts):
144156
assert type(parts[0]) is str
145157
assert "A" == parts[0]

tests/test_invoke.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,18 @@ def test_invoke_with_return_response(client):
3333
assert 5 == len(parts)
3434

3535

36+
def test_transaction(client):
37+
locks = None
38+
with client.transactions.create() as tx:
39+
client.invoke("/read_doc1.sjs", tx=tx)
40+
client.invoke("/read_doc2.xqy", tx=tx)
41+
locks = client.eval(javascript="xdmp.transactionLocks()", tx=tx)
42+
43+
read_locks = locks[0]["read"]
44+
assert "/doc1.json" in read_locks
45+
assert "/doc2.xml" in read_locks
46+
47+
3648
def __verify_invoke_with_vars(parts):
3749
assert "hello" == parts[0]
3850
assert "world" == parts[1]

tests/test_rows.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
from pytest import raises
2+
from marklogic.documents import Document
3+
import uuid
4+
25

36
dsl_query = 'op.fromView("test","musician").orderBy(op.col("lastName"))'
47
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"]}]}]}}'
@@ -80,6 +83,35 @@ def test_no_query_parameter_provided(client):
8083
client.rows.query()
8184

8285

86+
def test_transaction(client):
87+
lastName = str(uuid.uuid4())
88+
content = {
89+
"musician": {
90+
"lastName": lastName,
91+
"firstName": "DoesntMatter",
92+
"dob": "2000-01-01",
93+
}
94+
}
95+
uri = "/temp/musician5.json"
96+
97+
with client.transactions.create() as tx:
98+
query = f'op.fromView("test", "musician")'
99+
query = f'{query}.where(op.eq(op.col("lastName"), "{lastName}"))'
100+
results = client.rows.query(query, tx=tx)
101+
assert len(results) == 0
102+
103+
perms = {"python-tester": ["read", "update"]}
104+
doc = Document(uri, content, permissions=perms)
105+
client.documents.write(doc, tx=tx)
106+
107+
results = client.rows.query(query, tx=tx)
108+
assert len(results["rows"]) == 1
109+
110+
client.delete("/v1/documents", params={"uri": uri, "txid": tx.id})
111+
results = client.rows.query(query, tx=tx)
112+
assert len(results) == 0
113+
114+
83115
def verify_four_musicians_are_returned_in_json(data, column_name):
84116
assert type(data) is dict
85117
assert 4 == len(data["rows"])

0 commit comments

Comments
 (0)