Repository: googleapis/google-cloud-python

Issue:
Allow users to choose transaction isolation level?
I believe we currently only support the default transaction isolation level for Datastore (`snapshot`). Could we figure out a way to allow people to choose a `serializable` transaction?

See https://cloud.google.com/datastore/docs/apis/v1beta2/datasets/beginTransaction



Discussion:
[`Connection.begin_transaction`](https://github.com/GoogleCloudPlatform/gcloud-python/blob/2ef163918511f149415d32c32b74b6c9a23cb526/gcloud/datastore/connection.py#L270) already takes a `serializable` argument:  we just need to plumb that through to [`Transaction.__init__`](https://github.com/GoogleCloudPlatform/gcloud-python/blob/2ef163918511f149415d32c32b74b6c9a23cb526/gcloud/datastore/transaction.py#L103)/[`Transaction.begin`](https://github.com/GoogleCloudPlatform/gcloud-python/blob/2ef163918511f149415d32c32b74b6c9a23cb526/gcloud/datastore/transaction.py#L129).

... and [`Client.transaction`](https://github.com/GoogleCloudPlatform/gcloud-python/blob/2ef163918511f149415d32c32b74b6c9a23cb526/gcloud/datastore/transaction.py#L129).


Resolving patch:
diff --git a/gcloud/datastore/client.py b/gcloud/datastore/client.py
--- a/gcloud/datastore/client.py
+++ b/gcloud/datastore/client.py
@@ -440,12 +440,17 @@ def batch(self):
         """
         return Batch(self)
 
-    def transaction(self):
+    def transaction(self, serializable=False):
         """Proxy to :class:`gcloud.datastore.transaction.Transaction`.
 
         Passes our ``dataset_id``.
+
+        :type serializable: boolean
+        :param serializable: if true, perform this transaction at
+                             ``serializable`` isolation level;  otherwise,
+                             perform it at ``snapshot`` level.
         """
-        return Transaction(self)
+        return Transaction(self, serializable=serializable)
 
     def query(self, **kwargs):
         """Proxy to :class:`gcloud.datastore.query.Query`.
diff --git a/gcloud/datastore/transaction.py b/gcloud/datastore/transaction.py
--- a/gcloud/datastore/transaction.py
+++ b/gcloud/datastore/transaction.py
@@ -85,7 +85,12 @@ class Transaction(Batch):
       ...     transaction.commit()
 
     :type client: :class:`gcloud.datastore.client.Client`
-    :param client: The client used to connect to datastore.
+    :param client: the client used to connect to datastore.
+
+    :type serializable: boolean
+    :param serializable: if true, perform this transaction at
+                        ``serializable`` isolation level;  otherwise, perform
+                        it at ``snapshot`` level.
     """
 
     _INITIAL = 0
@@ -100,10 +105,11 @@ class Transaction(Batch):
     _FINISHED = 3
     """Enum value for _FINISHED status of transaction."""
 
-    def __init__(self, client):
+    def __init__(self, client, serializable=False):
         super(Transaction, self).__init__(client)
         self._id = None
         self._status = self._INITIAL
+        self._serializable = serializable
 
     @property
     def id(self):
@@ -114,6 +120,17 @@ def id(self):
         """
         return self._id
 
+    @property
+    def serializable(self):
+        """Should this transaction be run at ``serializable`` isolation
+
+        :rtype: boolean
+        :returns: if true, perform this transaction at
+                  ``serializable`` isolation level;  otherwise, perform
+                  it at ``snapshot`` level.
+        """
+        return self._serializable
+
     def current(self):
         """Return the topmost transaction.
 
@@ -138,7 +155,8 @@ def begin(self):
         if self._status != self._INITIAL:
             raise ValueError('Transaction already started previously.')
         self._status = self._IN_PROGRESS
-        self._id = self.connection.begin_transaction(self.dataset_id)
+        self._id = self.connection.begin_transaction(
+            self.dataset_id, serializable=self.serializable)
 
     def rollback(self):
         """Rolls back the current transaction.


Test patch:
diff --git a/gcloud/datastore/test_client.py b/gcloud/datastore/test_client.py
--- a/gcloud/datastore/test_client.py
+++ b/gcloud/datastore/test_client.py
@@ -790,7 +790,7 @@ def test_batch(self):
         self.assertEqual(batch.args, (client,))
         self.assertEqual(batch.kwargs, {})
 
-    def test_transaction(self):
+    def test_transaction_defaults(self):
         from gcloud.datastore import client as MUT
         from gcloud._testing import _Monkey
 
@@ -802,7 +802,21 @@ def test_transaction(self):
 
         self.assertTrue(isinstance(xact, _Dummy))
         self.assertEqual(xact.args, (client,))
-        self.assertEqual(xact.kwargs, {})
+        self.assertEqual(xact.kwargs, {'serializable': False})
+
+    def test_transaction_explicit(self):
+        from gcloud.datastore import client as MUT
+        from gcloud._testing import _Monkey
+
+        creds = object()
+        client = self._makeOne(credentials=creds)
+
+        with _Monkey(MUT, Transaction=_Dummy):
+            xact = client.transaction(serializable=True)
+
+        self.assertTrue(isinstance(xact, _Dummy))
+        self.assertEqual(xact.args, (client,))
+        self.assertEqual(xact.kwargs, {'serializable': True})
 
     def test_query_w_client(self):
         KIND = 'KIND'
diff --git a/gcloud/datastore/test_transaction.py b/gcloud/datastore/test_transaction.py
--- a/gcloud/datastore/test_transaction.py
+++ b/gcloud/datastore/test_transaction.py
@@ -21,10 +21,10 @@ def _getTargetClass(self):
         from gcloud.datastore.transaction import Transaction
         return Transaction
 
-    def _makeOne(self, client):
-        return self._getTargetClass()(client)
+    def _makeOne(self, client, **kw):
+        return self._getTargetClass()(client, **kw)
 
-    def test_ctor(self):
+    def test_ctor_defaults(self):
         from gcloud.datastore._datastore_v1_pb2 import Mutation
 
         _DATASET = 'DATASET'
@@ -37,6 +37,22 @@ def test_ctor(self):
         self.assertEqual(xact._status, self._getTargetClass()._INITIAL)
         self.assertTrue(isinstance(xact.mutation, Mutation))
         self.assertEqual(len(xact._auto_id_entities), 0)
+        self.assertFalse(xact.serializable)
+
+    def test_ctor_explicit(self):
+        from gcloud.datastore._datastore_v1_pb2 import Mutation
+
+        _DATASET = 'DATASET'
+        connection = _Connection()
+        client = _Client(_DATASET, connection)
+        xact = self._makeOne(client, serializable=True)
+        self.assertEqual(xact.dataset_id, _DATASET)
+        self.assertEqual(xact.connection, connection)
+        self.assertEqual(xact.id, None)
+        self.assertEqual(xact._status, self._getTargetClass()._INITIAL)
+        self.assertTrue(isinstance(xact.mutation, Mutation))
+        self.assertEqual(len(xact._auto_id_entities), 0)
+        self.assertTrue(xact.serializable)
 
     def test_current(self):
         from gcloud.datastore.test_client import _NoCommitBatch
@@ -64,14 +80,25 @@ def test_current(self):
         self.assertTrue(xact1.current() is None)
         self.assertTrue(xact2.current() is None)
 
-    def test_begin(self):
+    def test_begin_wo_serializable(self):
         _DATASET = 'DATASET'
         connection = _Connection(234)
         client = _Client(_DATASET, connection)
         xact = self._makeOne(client)
         xact.begin()
         self.assertEqual(xact.id, 234)
-        self.assertEqual(connection._begun, _DATASET)
+        self.assertEqual(connection._begun[0], _DATASET)
+        self.assertFalse(connection._begun[1])
+
+    def test_begin_w_serializable(self):
+        _DATASET = 'DATASET'
+        connection = _Connection(234)
+        client = _Client(_DATASET, connection)
+        xact = self._makeOne(client, serializable=True)
+        xact.begin()
+        self.assertEqual(xact.id, 234)
+        self.assertEqual(connection._begun[0], _DATASET)
+        self.assertTrue(connection._begun[1])
 
     def test_begin_tombstoned(self):
         _DATASET = 'DATASET'
@@ -80,7 +107,8 @@ def test_begin_tombstoned(self):
         xact = self._makeOne(client)
         xact.begin()
         self.assertEqual(xact.id, 234)
-        self.assertEqual(connection._begun, _DATASET)
+        self.assertEqual(connection._begun[0], _DATASET)
+        self.assertFalse(connection._begun[1])
 
         xact.rollback()
         self.assertEqual(xact.id, None)
@@ -134,7 +162,8 @@ def test_context_manager_no_raise(self):
         xact._mutation = mutation = object()
         with xact:
             self.assertEqual(xact.id, 234)
-            self.assertEqual(connection._begun, _DATASET)
+            self.assertEqual(connection._begun[0], _DATASET)
+            self.assertFalse(connection._begun[1])
         self.assertEqual(connection._committed, (_DATASET, mutation, 234))
         self.assertEqual(xact.id, None)
 
@@ -146,12 +175,13 @@ class Foo(Exception):
         _DATASET = 'DATASET'
         connection = _Connection(234)
         client = _Client(_DATASET, connection)
-        xact = self._makeOne(client)
+        xact = self._makeOne(client, serializable=True)
         xact._mutation = object()
         try:
             with xact:
                 self.assertEqual(xact.id, 234)
-                self.assertEqual(connection._begun, _DATASET)
+                self.assertEqual(connection._begun[0], _DATASET)
+                self.assertTrue(connection._begun[1])
                 raise Foo()
         except Foo:
             self.assertEqual(xact.id, None)
@@ -179,8 +209,8 @@ def __init__(self, xact_id=123):
         self._xact_id = xact_id
         self._commit_result = _CommitResult()
 
-    def begin_transaction(self, dataset_id):
-        self._begun = dataset_id
+    def begin_transaction(self, dataset_id, serializable):
+        self._begun = (dataset_id, serializable)
         return self._xact_id
 
     def rollback(self, dataset_id, transaction_id):
diff --git a/system_tests/datastore.py b/system_tests/datastore.py
--- a/system_tests/datastore.py
+++ b/system_tests/datastore.py
@@ -107,8 +107,8 @@ def test_post_with_id(self):
     def test_post_with_generated_id(self):
         self._generic_test_post()
 
-    def test_save_multiple(self):
-        with CLIENT.transaction() as xact:
+    def test_save_multiple_serializable(self):
+        with CLIENT.transaction(serializable=True) as xact:
             entity1 = self._get_post()
             xact.put(entity1)
             # Register entity to be deleted.