-
Notifications
You must be signed in to change notification settings - Fork 3
DEVEXP-561 Now supporting REST API transactions #33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Strangely, this doesn't match the documentation at https://docs.marklogic.com/REST/POST/v1/transactions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean the use of the "Accept" header? I may have eagerly added that, I'll try removing it.
I'm also going to change the code for when a transaction can't be committed / rolled back, seems like
raise_for_status()should be used there.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also that the documentation says to use the "Location" header in the response (which I find strange).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added comments, the docs are incorrect.