-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #33 from marklogic/feature/with-transaction
DEVEXP-561 Now supporting REST API transactions
- Loading branch information
Showing
6 changed files
with
315 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -251,6 +251,7 @@ GEM | |
|
||
PLATFORMS | ||
arm64-darwin-21 | ||
arm64-darwin-23 | ||
|
||
DEPENDENCIES | ||
github-pages (~> 228) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
--- | ||
layout: default | ||
title: Managing transactions | ||
nav_order: 4 | ||
--- | ||
|
||
The [/v1/transactions endpoint](https://docs.marklogic.com/REST/client/transaction-management) | ||
in the MarkLogic REST API supports managing a transaction that can be referenced in | ||
multiple separate calls to other REST API endpoints, with all calls being committed or | ||
rolled back together. The MarkLogic Python client simplifies usage of these endpoints | ||
via a `Transaction` class that is also a | ||
[Python context manager](https://docs.python.org/3/reference/datamodel.html#context-managers), | ||
thereby allowing it to handle committing or rolling back the transaction without any user | ||
involvement. | ||
|
||
The following example demonstrates writing documents via multiple calls to MarkLogic, | ||
all within the same REST API transaction; the example depends on first following the | ||
instructions in the [setup guide](example-setup.md): | ||
|
||
``` | ||
from marklogic import Client | ||
from marklogic.documents import Document | ||
client = Client('http://localhost:8000', digest=('python-user', 'pyth0n')) | ||
default_perms = {"rest-reader": ["read", "update"]} | ||
doc1 = Document("/tx/doc1.json", {"doc": 1}, permissions=default_perms) | ||
doc2 = Document("/tx/doc2.json", {"doc": 2}, permissions=default_perms) | ||
with client.transactions.create() as tx: | ||
client.documents.write(doc1, tx=tx).raise_for_status() | ||
client.documents.write(doc2, tx=tx).raise_for_status() | ||
``` | ||
|
||
The `client.transactions.create()` function returns a `Transaction` instance that acts | ||
as the context manager. When the `with` block completes, the `Transaction` instance | ||
calls the REST API to commit the transaction. | ||
|
||
As of 1.1.0, each of the functions in the `client.documents` object can include a | ||
reference to the transaction to ensure that the `read` or `write` or `search` operation | ||
occurs within the REST API transaction. | ||
|
||
## Ensuring a transaction is rolled back | ||
|
||
The `requests` function [`raise_for_status()`](https://requests.readthedocs.io/en/latest/user/quickstart/#errors-and-exceptions) | ||
is used in the example above to ensure that if a request fails, an error is thrown, | ||
causing the transaction to be rolled back. The following example demonstrates a rolled | ||
back transaction due to an invalid JSON object that causes a `write` operation to fail: | ||
|
||
``` | ||
doc1 = Document("/tx/doc1.json", {"doc": 1}, permissions=default_perms) | ||
doc2 = Document("/tx/doc2.json", "invalid json", permissions=default_perms) | ||
with client.transactions.create() as tx: | ||
client.documents.write(doc1, tx=tx).raise_for_status() | ||
client.documents.write(doc2, tx=tx).raise_for_status() | ||
``` | ||
|
||
The above will cause a `requests` `HTTPError` instance to be thrown, and the first | ||
document will not be written due to the transaction being rolled back. | ||
|
||
You are free to check the status code of the response object returned | ||
by each call as well; `raise_for_status()` is simply a commonly used convenience in the | ||
`requests` library. | ||
|
||
## Using the transaction request parameter | ||
|
||
You can reference the transaction when calling any REST API endpoint that supports the | ||
optional `txid` request parameter. The following example demonstrates this, reusing the | ||
same `client` instance from the first example: | ||
|
||
``` | ||
with client.transactions.create() as tx: | ||
client.post("/v1/resources/my-resource", params={"txid": tx.id}) | ||
client.delete("/v1/resources/other-resource", params={"txid": tx.id}) | ||
``` | ||
|
||
## Getting transaction status | ||
|
||
You can get the status of the transaction via the `get_status()` function: | ||
|
||
``` | ||
with client.transactions.create() as tx: | ||
print(f"Transaction status: {tx.get_status()}") | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import logging | ||
from requests import Response, Session | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
""" | ||
Defines classes to simplify usage of the REST endpoints defined at | ||
https://docs.marklogic.com/REST/client/transaction-management for managing transactions. | ||
""" | ||
|
||
|
||
class Transaction: | ||
""" | ||
Represents a transaction created via | ||
https://docs.marklogic.com/REST/POST/v1/transactions . | ||
An instance of this class can act as a Python context manager and can thus be used | ||
with the Python "with" keyword. This is the intended use case, allowing a user to | ||
perform one to many calls to MarkLogic within the "with" block, each referencing the | ||
ID associated with this transaction. When the "with" block concludes, the | ||
transaction will be automatically committed if no error was thrown, and rolled back | ||
otherwise. | ||
:param id: the ID of the new transaction, which is used for all subsequent | ||
operations involving the transaction. | ||
:param session: a requests Session object that is required for either committing or | ||
rolling back the transaction, as well as for obtaining status of the transaction. | ||
""" | ||
|
||
def __init__(self, id: str, session: Session): | ||
self.id = id | ||
self._session = session | ||
|
||
def __enter__(self): | ||
return self | ||
|
||
def get_status(self) -> dict: | ||
""" | ||
Retrieve transaction status via | ||
https://docs.marklogic.com/REST/GET/v1/transactions/[txid]. | ||
""" | ||
return self._session.get( | ||
f"/v1/transactions/{self.id}", headers={"Accept": "application/json"} | ||
).json() | ||
|
||
def commit(self) -> Response: | ||
""" | ||
Commits the transaction via | ||
https://docs.marklogic.com/REST/POST/v1/transactions/[txid]. This is expected to be | ||
invoked automatically via a Python context manager. | ||
""" | ||
logger.debug(f"Committing transaction with ID: {self.id}") | ||
return self._session.post( | ||
f"/v1/transactions/{self.id}", params={"result": "commit"} | ||
) | ||
|
||
def rollback(self) -> Response: | ||
""" | ||
Rolls back the transaction via | ||
https://docs.marklogic.com/REST/POST/v1/transactions/[txid]. This is expected to be | ||
invoked automatically via a Python context manager. | ||
""" | ||
logger.debug(f"Rolling back transaction with ID: {self.id}") | ||
return self._session.post( | ||
f"/v1/transactions/{self.id}", params={"result": "rollback"} | ||
) | ||
|
||
def __exit__(self, *args): | ||
response = ( | ||
self.rollback() | ||
if len(args) > 1 and isinstance(args[1], Exception) | ||
else self.commit() | ||
) | ||
assert ( | ||
204 == response.status_code | ||
), f"Could not end transaction; cause: {response.text}" | ||
|
||
|
||
class TransactionManager: | ||
def __init__(self, session: Session): | ||
self._session = session | ||
|
||
def create(self, name=None, time_limit=None, database=None) -> Transaction: | ||
""" | ||
Creates a new transaction via https://docs.marklogic.com/REST/POST/v1/transactions. | ||
Contrary to the docs, a Location header is not returned, but the transaction data | ||
is. And the Accept header can be used to control the format of the transaction data. | ||
The returned Transaction is a Python context manager and is intended to be used | ||
via the Python "with" keyword. | ||
:param name: optional name for the transaction. | ||
:param time_limit: optional time limit, in seconds, until the server cancels the | ||
transaction. | ||
:param database: optional database to associate with the transaction. | ||
""" | ||
params = {} | ||
if name: | ||
params["name"] = name | ||
if time_limit: | ||
params["timeLimit"] = time_limit | ||
if database: | ||
params["database"] = database | ||
|
||
response = self._session.post( | ||
"/v1/transactions", params=params, headers={"Accept": "application/json"} | ||
) | ||
id = response.json()["transaction-status"]["transaction-id"] | ||
return Transaction(id, self._session) |
Oops, something went wrong.