You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@beam.apache.org by al...@apache.org on 2017/02/23 01:23:08 UTC
[13/22] beam git commit: Rename google_cloud_dataflow and
google_cloud_platform
http://git-wip-us.apache.org/repos/asf/beam/blob/59ad58ac/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/datastoreio.py
----------------------------------------------------------------------
diff --git a/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/datastoreio.py b/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/datastoreio.py
deleted file mode 100644
index 335c532..0000000
--- a/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/datastoreio.py
+++ /dev/null
@@ -1,397 +0,0 @@
-#
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements. See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-"""A connector for reading from and writing to Google Cloud Datastore"""
-
-import logging
-
-# Protect against environments where datastore library is not available.
-# pylint: disable=wrong-import-order, wrong-import-position
-try:
- from google.cloud.proto.datastore.v1 import datastore_pb2
- from googledatastore import helper as datastore_helper
-except ImportError:
- pass
-# pylint: enable=wrong-import-order, wrong-import-position
-
-from apache_beam.io.google_cloud_platform.datastore.v1 import helper
-from apache_beam.io.google_cloud_platform.datastore.v1 import query_splitter
-from apache_beam.transforms import Create
-from apache_beam.transforms import DoFn
-from apache_beam.transforms import FlatMap
-from apache_beam.transforms import GroupByKey
-from apache_beam.transforms import Map
-from apache_beam.transforms import PTransform
-from apache_beam.transforms import ParDo
-from apache_beam.transforms.util import Values
-
-__all__ = ['ReadFromDatastore', 'WriteToDatastore', 'DeleteFromDatastore']
-
-
-class ReadFromDatastore(PTransform):
- """A ``PTransform`` for reading from Google Cloud Datastore.
-
- To read a ``PCollection[Entity]`` from a Cloud Datastore ``Query``, use
- ``ReadFromDatastore`` transform by providing a `project` id and a `query` to
- read from. You can optionally provide a `namespace` and/or specify how many
- splits you want for the query through `num_splits` option.
-
- Note: Normally, a runner will read from Cloud Datastore in parallel across
- many workers. However, when the `query` is configured with a `limit` or if the
- query contains inequality filters like `GREATER_THAN, LESS_THAN` etc., then
- all the returned results will be read by a single worker in order to ensure
- correct data. Since data is read from a single worker, this could have
- significant impact on the performance of the job.
-
- The semantics for the query splitting is defined below:
- 1. If `num_splits` is equal to 0, then the number of splits will be chosen
- dynamically at runtime based on the query data size.
-
- 2. Any value of `num_splits` greater than
- `ReadFromDatastore._NUM_QUERY_SPLITS_MAX` will be capped at that value.
-
- 3. If the `query` has a user limit set, or contains inequality filters, then
- `num_splits` will be ignored and no split will be performed.
-
- 4. Under certain cases Cloud Datastore is unable to split query to the
- requested number of splits. In such cases we just use whatever the Cloud
- Datastore returns.
-
- See https://developers.google.com/datastore/ for more details on Google Cloud
- Datastore.
- """
-
- # An upper bound on the number of splits for a query.
- _NUM_QUERY_SPLITS_MAX = 50000
- # A lower bound on the number of splits for a query. This is to ensure that
- # we parellelize the query even when Datastore statistics are not available.
- _NUM_QUERY_SPLITS_MIN = 12
- # Default bundle size of 64MB.
- _DEFAULT_BUNDLE_SIZE_BYTES = 64 * 1024 * 1024
-
- def __init__(self, project, query, namespace=None, num_splits=0):
- """Initialize the ReadFromDatastore transform.
-
- Args:
- project: The Project ID
- query: Cloud Datastore query to be read from.
- namespace: An optional namespace.
- num_splits: Number of splits for the query.
- """
- logging.warning('datastoreio read transform is experimental.')
- super(ReadFromDatastore, self).__init__()
-
- if not project:
- ValueError("Project cannot be empty")
- if not query:
- ValueError("Query cannot be empty")
- if num_splits < 0:
- ValueError("num_splits must be greater than or equal 0")
-
- self._project = project
- # using _namespace conflicts with DisplayData._namespace
- self._datastore_namespace = namespace
- self._query = query
- self._num_splits = num_splits
-
- def expand(self, pcoll):
- # This is a composite transform involves the following:
- # 1. Create a singleton of the user provided `query` and apply a ``ParDo``
- # that splits the query into `num_splits` and assign each split query a
- # unique `int` as the key. The resulting output is of the type
- # ``PCollection[(int, Query)]``.
- #
- # If the value of `num_splits` is less than or equal to 0, then the
- # number of splits will be computed dynamically based on the size of the
- # data for the `query`.
- #
- # 2. The resulting ``PCollection`` is sharded using a ``GroupByKey``
- # operation. The queries are extracted from the (int, Iterable[Query]) and
- # flattened to output a ``PCollection[Query]``.
- #
- # 3. In the third step, a ``ParDo`` reads entities for each query and
- # outputs a ``PCollection[Entity]``.
-
- queries = (pcoll.pipeline
- | 'User Query' >> Create([self._query])
- | 'Split Query' >> ParDo(ReadFromDatastore.SplitQueryFn(
- self._project, self._query, self._datastore_namespace,
- self._num_splits)))
-
- sharded_queries = (queries
- | GroupByKey()
- | Values()
- | 'flatten' >> FlatMap(lambda x: x))
-
- entities = sharded_queries | 'Read' >> ParDo(
- ReadFromDatastore.ReadFn(self._project, self._datastore_namespace))
- return entities
-
- def display_data(self):
- disp_data = {'project': self._project,
- 'query': str(self._query),
- 'num_splits': self._num_splits}
-
- if self._datastore_namespace is not None:
- disp_data['namespace'] = self._datastore_namespace
-
- return disp_data
-
- class SplitQueryFn(DoFn):
- """A `DoFn` that splits a given query into multiple sub-queries."""
- def __init__(self, project, query, namespace, num_splits):
- super(ReadFromDatastore.SplitQueryFn, self).__init__()
- self._datastore = None
- self._project = project
- self._datastore_namespace = namespace
- self._query = query
- self._num_splits = num_splits
-
- def start_bundle(self):
- self._datastore = helper.get_datastore(self._project)
-
- def process(self, query, *args, **kwargs):
- # distinct key to be used to group query splits.
- key = 1
-
- # If query has a user set limit, then the query cannot be split.
- if query.HasField('limit'):
- return [(key, query)]
-
- # Compute the estimated numSplits if not specified by the user.
- if self._num_splits == 0:
- estimated_num_splits = ReadFromDatastore.get_estimated_num_splits(
- self._project, self._datastore_namespace, self._query,
- self._datastore)
- else:
- estimated_num_splits = self._num_splits
-
- logging.info("Splitting the query into %d splits", estimated_num_splits)
- try:
- query_splits = query_splitter.get_splits(
- self._datastore, query, estimated_num_splits,
- helper.make_partition(self._project, self._datastore_namespace))
- except Exception:
- logging.warning("Unable to parallelize the given query: %s", query,
- exc_info=True)
- query_splits = [query]
-
- sharded_query_splits = []
- for split_query in query_splits:
- sharded_query_splits.append((key, split_query))
- key += 1
-
- return sharded_query_splits
-
- def display_data(self):
- disp_data = {'project': self._project,
- 'query': str(self._query),
- 'num_splits': self._num_splits}
-
- if self._datastore_namespace is not None:
- disp_data['namespace'] = self._datastore_namespace
-
- return disp_data
-
- class ReadFn(DoFn):
- """A DoFn that reads entities from Cloud Datastore, for a given query."""
- def __init__(self, project, namespace=None):
- super(ReadFromDatastore.ReadFn, self).__init__()
- self._project = project
- self._datastore_namespace = namespace
- self._datastore = None
-
- def start_bundle(self):
- self._datastore = helper.get_datastore(self._project)
-
- def process(self, query, *args, **kwargs):
- # Returns an iterator of entities that reads in batches.
- entities = helper.fetch_entities(self._project, self._datastore_namespace,
- query, self._datastore)
- return entities
-
- def display_data(self):
- disp_data = {'project': self._project}
-
- if self._datastore_namespace is not None:
- disp_data['namespace'] = self._datastore_namespace
-
- return disp_data
-
- @staticmethod
- def query_latest_statistics_timestamp(project, namespace, datastore):
- """Fetches the latest timestamp of statistics from Cloud Datastore.
-
- Cloud Datastore system tables with statistics are periodically updated.
- This method fethes the latest timestamp (in microseconds) of statistics
- update using the `__Stat_Total__` table.
- """
- query = helper.make_latest_timestamp_query(namespace)
- req = helper.make_request(project, namespace, query)
- resp = datastore.run_query(req)
- if len(resp.batch.entity_results) == 0:
- raise RuntimeError("Datastore total statistics unavailable.")
-
- entity = resp.batch.entity_results[0].entity
- return datastore_helper.micros_from_timestamp(
- entity.properties['timestamp'].timestamp_value)
-
- @staticmethod
- def get_estimated_size_bytes(project, namespace, query, datastore):
- """Get the estimated size of the data returned by the given query.
-
- Cloud Datastore provides no way to get a good estimate of how large the
- result of a query is going to be. Hence we use the __Stat_Kind__ system
- table to get size of the entire kind as an approximate estimate, assuming
- exactly 1 kind is specified in the query.
- See https://cloud.google.com/datastore/docs/concepts/stats.
- """
- kind = query.kind[0].name
- latest_timestamp = ReadFromDatastore.query_latest_statistics_timestamp(
- project, namespace, datastore)
- logging.info('Latest stats timestamp for kind %s is %s',
- kind, latest_timestamp)
-
- kind_stats_query = (
- helper.make_kind_stats_query(namespace, kind, latest_timestamp))
-
- req = helper.make_request(project, namespace, kind_stats_query)
- resp = datastore.run_query(req)
- if len(resp.batch.entity_results) == 0:
- raise RuntimeError("Datastore statistics for kind %s unavailable" % kind)
-
- entity = resp.batch.entity_results[0].entity
- return datastore_helper.get_value(entity.properties['entity_bytes'])
-
- @staticmethod
- def get_estimated_num_splits(project, namespace, query, datastore):
- """Computes the number of splits to be performed on the given query."""
- try:
- estimated_size_bytes = ReadFromDatastore.get_estimated_size_bytes(
- project, namespace, query, datastore)
- logging.info('Estimated size bytes for query: %s', estimated_size_bytes)
- num_splits = int(min(ReadFromDatastore._NUM_QUERY_SPLITS_MAX, round(
- (float(estimated_size_bytes) /
- ReadFromDatastore._DEFAULT_BUNDLE_SIZE_BYTES))))
-
- except Exception as e:
- logging.warning('Failed to fetch estimated size bytes: %s', e)
- # Fallback in case estimated size is unavailable.
- num_splits = ReadFromDatastore._NUM_QUERY_SPLITS_MIN
-
- return max(num_splits, ReadFromDatastore._NUM_QUERY_SPLITS_MIN)
-
-
-class _Mutate(PTransform):
- """A ``PTransform`` that writes mutations to Cloud Datastore.
-
- Only idempotent Datastore mutation operations (upsert and delete) are
- supported, as the commits are retried when failures occur.
- """
-
- # Max allowed Datastore write batch size.
- _WRITE_BATCH_SIZE = 500
-
- def __init__(self, project, mutation_fn):
- """Initializes a Mutate transform.
-
- Args:
- project: The Project ID
- mutation_fn: A function that converts `entities` or `keys` to
- `mutations`.
- """
- self._project = project
- self._mutation_fn = mutation_fn
- logging.warning('datastoreio write transform is experimental.')
-
- def expand(self, pcoll):
- return (pcoll
- | 'Convert to Mutation' >> Map(self._mutation_fn)
- | 'Write Mutation to Datastore' >> ParDo(_Mutate.DatastoreWriteFn(
- self._project)))
-
- def display_data(self):
- return {'project': self._project,
- 'mutation_fn': self._mutation_fn.__class__.__name__}
-
- class DatastoreWriteFn(DoFn):
- """A ``DoFn`` that write mutations to Datastore.
-
- Mutations are written in batches, where the maximum batch size is
- `Mutate._WRITE_BATCH_SIZE`.
-
- Commits are non-transactional. If a commit fails because of a conflict over
- an entity group, the commit will be retried. This means that the mutation
- should be idempotent (`upsert` and `delete` mutations) to prevent duplicate
- data or errors.
- """
- def __init__(self, project):
- self._project = project
- self._datastore = None
- self._mutations = []
-
- def start_bundle(self):
- self._mutations = []
- self._datastore = helper.get_datastore(self._project)
-
- def process(self, element):
- self._mutations.append(element)
- if len(self._mutations) >= _Mutate._WRITE_BATCH_SIZE:
- self._flush_batch()
-
- def finish_bundle(self):
- if self._mutations:
- self._flush_batch()
- self._mutations = []
-
- def _flush_batch(self):
- # Flush the current batch of mutations to Cloud Datastore.
- helper.write_mutations(self._datastore, self._project, self._mutations)
- logging.debug("Successfully wrote %d mutations.", len(self._mutations))
- self._mutations = []
-
-
-class WriteToDatastore(_Mutate):
- """A ``PTransform`` to write a ``PCollection[Entity]`` to Cloud Datastore."""
- def __init__(self, project):
- super(WriteToDatastore, self).__init__(
- project, WriteToDatastore.to_upsert_mutation)
-
- @staticmethod
- def to_upsert_mutation(entity):
- if not helper.is_key_valid(entity.key):
- raise ValueError('Entities to be written to the Cloud Datastore must '
- 'have complete keys:\n%s' % entity)
- mutation = datastore_pb2.Mutation()
- mutation.upsert.CopyFrom(entity)
- return mutation
-
-
-class DeleteFromDatastore(_Mutate):
- """A ``PTransform`` to delete a ``PCollection[Key]`` from Cloud Datastore."""
- def __init__(self, project):
- super(DeleteFromDatastore, self).__init__(
- project, DeleteFromDatastore.to_delete_mutation)
-
- @staticmethod
- def to_delete_mutation(key):
- if not helper.is_key_valid(key):
- raise ValueError('Keys to be deleted from the Cloud Datastore must be '
- 'complete:\n%s", key')
- mutation = datastore_pb2.Mutation()
- mutation.delete.CopyFrom(key)
- return mutation
http://git-wip-us.apache.org/repos/asf/beam/blob/59ad58ac/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/datastoreio_test.py
----------------------------------------------------------------------
diff --git a/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/datastoreio_test.py b/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/datastoreio_test.py
deleted file mode 100644
index 5afb146..0000000
--- a/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/datastoreio_test.py
+++ /dev/null
@@ -1,245 +0,0 @@
-#
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements. See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-import unittest
-
-from mock import MagicMock, call, patch
-
-from apache_beam.io.google_cloud_platform.datastore.v1 import fake_datastore
-from apache_beam.io.google_cloud_platform.datastore.v1 import helper
-from apache_beam.io.google_cloud_platform.datastore.v1 import query_splitter
-from apache_beam.io.google_cloud_platform.datastore.v1.datastoreio import _Mutate
-from apache_beam.io.google_cloud_platform.datastore.v1.datastoreio import ReadFromDatastore
-from apache_beam.io.google_cloud_platform.datastore.v1.datastoreio import WriteToDatastore
-
-# Protect against environments where datastore library is not available.
-# pylint: disable=wrong-import-order, wrong-import-position
-try:
- from google.cloud.proto.datastore.v1 import datastore_pb2
- from google.cloud.proto.datastore.v1 import query_pb2
- from google.protobuf import timestamp_pb2
- from googledatastore import helper as datastore_helper
-except ImportError:
- datastore_pb2 = None
-# pylint: enable=wrong-import-order, wrong-import-position
-
-
-@unittest.skipIf(datastore_pb2 is None, 'GCP dependencies are not installed')
-class DatastoreioTest(unittest.TestCase):
- _PROJECT = 'project'
- _KIND = 'kind'
- _NAMESPACE = 'namespace'
-
- def setUp(self):
- self._mock_datastore = MagicMock()
- self._query = query_pb2.Query()
- self._query.kind.add().name = self._KIND
-
- def test_get_estimated_size_bytes_without_namespace(self):
- entity_bytes = 100
- timestamp = timestamp_pb2.Timestamp(seconds=1234)
- self.check_estimated_size_bytes(entity_bytes, timestamp)
-
- def test_get_estimated_size_bytes_with_namespace(self):
- entity_bytes = 100
- timestamp = timestamp_pb2.Timestamp(seconds=1234)
- self.check_estimated_size_bytes(entity_bytes, timestamp, self._NAMESPACE)
-
- def test_SplitQueryFn_with_num_splits(self):
- with patch.object(helper, 'get_datastore',
- return_value=self._mock_datastore):
- num_splits = 23
-
- def fake_get_splits(datastore, query, num_splits, partition=None):
- return self.split_query(query, num_splits)
-
- with patch.object(query_splitter, 'get_splits',
- side_effect=fake_get_splits):
-
- split_query_fn = ReadFromDatastore.SplitQueryFn(
- self._PROJECT, self._query, None, num_splits)
- split_query_fn.start_bundle()
- returned_split_queries = []
- for split_query in split_query_fn.process(self._query):
- returned_split_queries.append(split_query)
-
- self.assertEqual(len(returned_split_queries), num_splits)
- self.assertEqual(0, len(self._mock_datastore.run_query.call_args_list))
- self.verify_unique_keys(returned_split_queries)
-
- def test_SplitQueryFn_without_num_splits(self):
- with patch.object(helper, 'get_datastore',
- return_value=self._mock_datastore):
- # Force SplitQueryFn to compute the number of query splits
- num_splits = 0
- expected_num_splits = 23
- entity_bytes = (expected_num_splits *
- ReadFromDatastore._DEFAULT_BUNDLE_SIZE_BYTES)
- with patch.object(ReadFromDatastore, 'get_estimated_size_bytes',
- return_value=entity_bytes):
-
- def fake_get_splits(datastore, query, num_splits, partition=None):
- return self.split_query(query, num_splits)
-
- with patch.object(query_splitter, 'get_splits',
- side_effect=fake_get_splits):
- split_query_fn = ReadFromDatastore.SplitQueryFn(
- self._PROJECT, self._query, None, num_splits)
- split_query_fn.start_bundle()
- returned_split_queries = []
- for split_query in split_query_fn.process(self._query):
- returned_split_queries.append(split_query)
-
- self.assertEqual(len(returned_split_queries), expected_num_splits)
- self.assertEqual(0,
- len(self._mock_datastore.run_query.call_args_list))
- self.verify_unique_keys(returned_split_queries)
-
- def test_SplitQueryFn_with_query_limit(self):
- """A test that verifies no split is performed when the query has a limit."""
- with patch.object(helper, 'get_datastore',
- return_value=self._mock_datastore):
- self._query.limit.value = 3
- split_query_fn = ReadFromDatastore.SplitQueryFn(
- self._PROJECT, self._query, None, 4)
- split_query_fn.start_bundle()
- returned_split_queries = []
- for split_query in split_query_fn.process(self._query):
- returned_split_queries.append(split_query)
-
- self.assertEqual(1, len(returned_split_queries))
- self.assertEqual(0, len(self._mock_datastore.method_calls))
-
- def test_SplitQueryFn_with_exception(self):
- """A test that verifies that no split is performed when failures occur."""
- with patch.object(helper, 'get_datastore',
- return_value=self._mock_datastore):
- # Force SplitQueryFn to compute the number of query splits
- num_splits = 0
- expected_num_splits = 1
- entity_bytes = (expected_num_splits *
- ReadFromDatastore._DEFAULT_BUNDLE_SIZE_BYTES)
- with patch.object(ReadFromDatastore, 'get_estimated_size_bytes',
- return_value=entity_bytes):
-
- with patch.object(query_splitter, 'get_splits',
- side_effect=ValueError("Testing query split error")):
- split_query_fn = ReadFromDatastore.SplitQueryFn(
- self._PROJECT, self._query, None, num_splits)
- split_query_fn.start_bundle()
- returned_split_queries = []
- for split_query in split_query_fn.process(self._query):
- returned_split_queries.append(split_query)
-
- self.assertEqual(len(returned_split_queries), expected_num_splits)
- self.assertEqual(returned_split_queries[0][1], self._query)
- self.assertEqual(0,
- len(self._mock_datastore.run_query.call_args_list))
- self.verify_unique_keys(returned_split_queries)
-
- def test_DatastoreWriteFn_with_emtpy_batch(self):
- self.check_DatastoreWriteFn(0)
-
- def test_DatastoreWriteFn_with_one_batch(self):
- num_entities_to_write = _Mutate._WRITE_BATCH_SIZE * 1 - 50
- self.check_DatastoreWriteFn(num_entities_to_write)
-
- def test_DatastoreWriteFn_with_multiple_batches(self):
- num_entities_to_write = _Mutate._WRITE_BATCH_SIZE * 3 + 50
- self.check_DatastoreWriteFn(num_entities_to_write)
-
- def test_DatastoreWriteFn_with_batch_size_exact_multiple(self):
- num_entities_to_write = _Mutate._WRITE_BATCH_SIZE * 2
- self.check_DatastoreWriteFn(num_entities_to_write)
-
- def check_DatastoreWriteFn(self, num_entities):
- """A helper function to test DatastoreWriteFn."""
-
- with patch.object(helper, 'get_datastore',
- return_value=self._mock_datastore):
- entities = [e.entity for e in
- fake_datastore.create_entities(num_entities)]
-
- expected_mutations = map(WriteToDatastore.to_upsert_mutation, entities)
- actual_mutations = []
-
- self._mock_datastore.commit.side_effect = (
- fake_datastore.create_commit(actual_mutations))
-
- datastore_write_fn = _Mutate.DatastoreWriteFn(self._PROJECT)
-
- datastore_write_fn.start_bundle()
- for mutation in expected_mutations:
- datastore_write_fn.process(mutation)
- datastore_write_fn.finish_bundle()
-
- self.assertEqual(actual_mutations, expected_mutations)
- self.assertEqual((num_entities - 1) / _Mutate._WRITE_BATCH_SIZE + 1,
- self._mock_datastore.commit.call_count)
-
- def verify_unique_keys(self, queries):
- """A helper function that verifies if all the queries have unique keys."""
- keys, _ = zip(*queries)
- keys = set(keys)
- self.assertEqual(len(keys), len(queries))
-
- def check_estimated_size_bytes(self, entity_bytes, timestamp, namespace=None):
- """A helper method to test get_estimated_size_bytes"""
-
- timestamp_req = helper.make_request(
- self._PROJECT, namespace, helper.make_latest_timestamp_query(namespace))
- timestamp_resp = self.make_stats_response(
- {'timestamp': datastore_helper.from_timestamp(timestamp)})
- kind_stat_req = helper.make_request(
- self._PROJECT, namespace, helper.make_kind_stats_query(
- namespace, self._query.kind[0].name,
- datastore_helper.micros_from_timestamp(timestamp)))
- kind_stat_resp = self.make_stats_response(
- {'entity_bytes': entity_bytes})
-
- def fake_run_query(req):
- if req == timestamp_req:
- return timestamp_resp
- elif req == kind_stat_req:
- return kind_stat_resp
- else:
- print kind_stat_req
- raise ValueError("Unknown req: %s" % req)
-
- self._mock_datastore.run_query.side_effect = fake_run_query
- self.assertEqual(entity_bytes, ReadFromDatastore.get_estimated_size_bytes(
- self._PROJECT, namespace, self._query, self._mock_datastore))
- self.assertEqual(self._mock_datastore.run_query.call_args_list,
- [call(timestamp_req), call(kind_stat_req)])
-
- def make_stats_response(self, property_map):
- resp = datastore_pb2.RunQueryResponse()
- entity_result = resp.batch.entity_results.add()
- datastore_helper.add_properties(entity_result.entity, property_map)
- return resp
-
- def split_query(self, query, num_splits):
- """Generate dummy query splits."""
- split_queries = []
- for _ in range(0, num_splits):
- q = query_pb2.Query()
- q.CopyFrom(query)
- split_queries.append(q)
- return split_queries
-
-if __name__ == '__main__':
- unittest.main()
http://git-wip-us.apache.org/repos/asf/beam/blob/59ad58ac/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/fake_datastore.py
----------------------------------------------------------------------
diff --git a/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/fake_datastore.py b/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/fake_datastore.py
deleted file mode 100644
index bc4d07f..0000000
--- a/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/fake_datastore.py
+++ /dev/null
@@ -1,98 +0,0 @@
-#
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements. See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-"""Fake datastore used for unit testing."""
-import uuid
-
-# Protect against environments where datastore library is not available.
-# pylint: disable=wrong-import-order, wrong-import-position
-try:
- from google.cloud.proto.datastore.v1 import datastore_pb2
- from google.cloud.proto.datastore.v1 import query_pb2
-except ImportError:
- pass
-# pylint: enable=wrong-import-order, wrong-import-position
-
-
-def create_run_query(entities, batch_size):
- """A fake datastore run_query method that returns entities in batches.
-
- Note: the outer method is needed to make the `entities` and `batch_size`
- available in the scope of fake_run_query method.
-
- Args:
- entities: list of entities supposed to be contained in the datastore.
- batch_size: the number of entities that run_query method returns in one
- request.
- """
- def run_query(req):
- start = int(req.query.start_cursor) if req.query.start_cursor else 0
- # if query limit is less than batch_size, then only return that much.
- count = min(batch_size, req.query.limit.value)
- # cannot go more than the number of entities contained in datastore.
- end = min(len(entities), start + count)
- finish = False
- # Finish reading when there are no more entities to return,
- # or request query limit has been satisfied.
- if end == len(entities) or count == req.query.limit.value:
- finish = True
- return create_response(entities[start:end], str(end), finish)
- return run_query
-
-
-def create_commit(mutations):
- """A fake Datastore commit method that writes the mutations to a list.
-
- Args:
- mutations: A list to write mutations to.
-
- Returns:
- A fake Datastore commit method
- """
-
- def commit(req):
- for mutation in req.mutations:
- mutations.append(mutation)
-
- return commit
-
-
-def create_response(entities, end_cursor, finish):
- """Creates a query response for a given batch of scatter entities."""
- resp = datastore_pb2.RunQueryResponse()
- if finish:
- resp.batch.more_results = query_pb2.QueryResultBatch.NO_MORE_RESULTS
- else:
- resp.batch.more_results = query_pb2.QueryResultBatch.NOT_FINISHED
-
- resp.batch.end_cursor = end_cursor
- for entity_result in entities:
- resp.batch.entity_results.add().CopyFrom(entity_result)
-
- return resp
-
-
-def create_entities(count):
- """Creates a list of entities with random keys."""
- entities = []
-
- for _ in range(count):
- entity_result = query_pb2.EntityResult()
- entity_result.entity.key.path.add().name = str(uuid.uuid4())
- entities.append(entity_result)
-
- return entities
http://git-wip-us.apache.org/repos/asf/beam/blob/59ad58ac/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/helper.py
----------------------------------------------------------------------
diff --git a/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/helper.py b/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/helper.py
deleted file mode 100644
index e15e43b..0000000
--- a/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/helper.py
+++ /dev/null
@@ -1,274 +0,0 @@
-#
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements. See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-"""Cloud Datastore helper functions."""
-import sys
-
-# Protect against environments where datastore library is not available.
-# pylint: disable=wrong-import-order, wrong-import-position
-try:
- from google.cloud.proto.datastore.v1 import datastore_pb2
- from google.cloud.proto.datastore.v1 import entity_pb2
- from google.cloud.proto.datastore.v1 import query_pb2
- from googledatastore import PropertyFilter, CompositeFilter
- from googledatastore import helper as datastore_helper
- from googledatastore.connection import Datastore
- from googledatastore.connection import RPCError
- QUERY_NOT_FINISHED = query_pb2.QueryResultBatch.NOT_FINISHED
-except ImportError:
- QUERY_NOT_FINISHED = None
-# pylint: enable=wrong-import-order, wrong-import-position
-
-from apache_beam.internal import auth
-from apache_beam.utils import retry
-
-
-def key_comparator(k1, k2):
- """A comparator for Datastore keys.
-
- Comparison is only valid for keys in the same partition. The comparison here
- is between the list of paths for each key.
- """
-
- if k1.partition_id != k2.partition_id:
- raise ValueError('Cannot compare keys with different partition ids.')
-
- k2_iter = iter(k2.path)
-
- for k1_path in k1.path:
- k2_path = next(k2_iter, None)
- if not k2_path:
- return 1
-
- result = compare_path(k1_path, k2_path)
-
- if result != 0:
- return result
-
- k2_path = next(k2_iter, None)
- if k2_path:
- return -1
- else:
- return 0
-
-
-def compare_path(p1, p2):
- """A comparator for key path.
-
- A path has either an `id` or a `name` field defined. The
- comparison works with the following rules:
-
- 1. If one path has `id` defined while the other doesn't, then the
- one with `id` defined is considered smaller.
- 2. If both paths have `id` defined, then their ids are compared.
- 3. If no `id` is defined for both paths, then their `names` are compared.
- """
-
- result = str_compare(p1.kind, p2.kind)
- if result != 0:
- return result
-
- if p1.HasField('id'):
- if not p2.HasField('id'):
- return -1
-
- return p1.id - p2.id
-
- if p2.HasField('id'):
- return 1
-
- return str_compare(p1.name, p2.name)
-
-
-def str_compare(s1, s2):
- if s1 == s2:
- return 0
- elif s1 < s2:
- return -1
- else:
- return 1
-
-
-def get_datastore(project):
- """Returns a Cloud Datastore client."""
- credentials = auth.get_service_credentials()
- return Datastore(project, credentials)
-
-
-def make_request(project, namespace, query):
- """Make a Cloud Datastore request for the given query."""
- req = datastore_pb2.RunQueryRequest()
- req.partition_id.CopyFrom(make_partition(project, namespace))
-
- req.query.CopyFrom(query)
- return req
-
-
-def make_partition(project, namespace):
- """Make a PartitionId for the given project and namespace."""
- partition = entity_pb2.PartitionId()
- partition.project_id = project
- if namespace is not None:
- partition.namespace_id = namespace
-
- return partition
-
-
-def retry_on_rpc_error(exception):
- """A retry filter for Cloud Datastore RPCErrors."""
- if isinstance(exception, RPCError):
- if exception.code >= 500:
- return True
- else:
- return False
- else:
- # TODO(vikasrk): Figure out what other errors should be retried.
- return False
-
-
-def fetch_entities(project, namespace, query, datastore):
- """A helper method to fetch entities from Cloud Datastore.
-
- Args:
- project: Project ID
- namespace: Cloud Datastore namespace
- query: Query to be read from
- datastore: Cloud Datastore Client
-
- Returns:
- An iterator of entities.
- """
- return QueryIterator(project, namespace, query, datastore)
-
-
-def is_key_valid(key):
- """Returns True if a Cloud Datastore key is complete.
-
- A key is complete if its last element has either an id or a name.
- """
- if not key.path:
- return False
- return key.path[-1].HasField('id') or key.path[-1].HasField('name')
-
-
-def write_mutations(datastore, project, mutations):
- """A helper function to write a batch of mutations to Cloud Datastore.
-
- If a commit fails, it will be retried upto 5 times. All mutations in the
- batch will be committed again, even if the commit was partially successful.
- If the retry limit is exceeded, the last exception from Cloud Datastore will
- be raised.
- """
- commit_request = datastore_pb2.CommitRequest()
- commit_request.mode = datastore_pb2.CommitRequest.NON_TRANSACTIONAL
- commit_request.project_id = project
- for mutation in mutations:
- commit_request.mutations.add().CopyFrom(mutation)
-
- @retry.with_exponential_backoff(num_retries=5,
- retry_filter=retry_on_rpc_error)
- def commit(req):
- datastore.commit(req)
-
- commit(commit_request)
-
-
-def make_latest_timestamp_query(namespace):
- """Make a Query to fetch the latest timestamp statistics."""
- query = query_pb2.Query()
- if namespace is None:
- query.kind.add().name = '__Stat_Total__'
- else:
- query.kind.add().name = '__Stat_Ns_Total__'
-
- # Descending order of `timestamp`
- datastore_helper.add_property_orders(query, "-timestamp")
- # Only get the latest entity
- query.limit.value = 1
- return query
-
-
-def make_kind_stats_query(namespace, kind, latest_timestamp):
- """Make a Query to fetch the latest kind statistics."""
- kind_stat_query = query_pb2.Query()
- if namespace is None:
- kind_stat_query.kind.add().name = '__Stat_Kind__'
- else:
- kind_stat_query.kind.add().name = '__Stat_Ns_Kind__'
-
- kind_filter = datastore_helper.set_property_filter(
- query_pb2.Filter(), 'kind_name', PropertyFilter.EQUAL, unicode(kind))
- timestamp_filter = datastore_helper.set_property_filter(
- query_pb2.Filter(), 'timestamp', PropertyFilter.EQUAL,
- latest_timestamp)
-
- datastore_helper.set_composite_filter(kind_stat_query.filter,
- CompositeFilter.AND, kind_filter,
- timestamp_filter)
- return kind_stat_query
-
-
-class QueryIterator(object):
- """A iterator class for entities of a given query.
-
- Entities are read in batches. Retries on failures.
- """
- _NOT_FINISHED = QUERY_NOT_FINISHED
- # Maximum number of results to request per query.
- _BATCH_SIZE = 500
-
- def __init__(self, project, namespace, query, datastore):
- self._query = query
- self._datastore = datastore
- self._project = project
- self._namespace = namespace
- self._start_cursor = None
- self._limit = self._query.limit.value or sys.maxint
- self._req = make_request(project, namespace, query)
-
- @retry.with_exponential_backoff(num_retries=5,
- retry_filter=retry_on_rpc_error)
- def _next_batch(self):
- """Fetches the next batch of entities."""
- if self._start_cursor is not None:
- self._req.query.start_cursor = self._start_cursor
-
- # set batch size
- self._req.query.limit.value = min(self._BATCH_SIZE, self._limit)
- resp = self._datastore.run_query(self._req)
- return resp
-
- def __iter__(self):
- more_results = True
- while more_results:
- resp = self._next_batch()
- for entity_result in resp.batch.entity_results:
- yield entity_result.entity
-
- self._start_cursor = resp.batch.end_cursor
- num_results = len(resp.batch.entity_results)
- self._limit -= num_results
-
- # Check if we need to read more entities.
- # True when query limit hasn't been satisfied and there are more entities
- # to be read. The latter is true if the response has a status
- # `NOT_FINISHED` or if the number of results read in the previous batch
- # is equal to `_BATCH_SIZE` (all indications that there is more data be
- # read).
- more_results = ((self._limit > 0) and
- ((num_results == self._BATCH_SIZE) or
- (resp.batch.more_results == self._NOT_FINISHED)))
http://git-wip-us.apache.org/repos/asf/beam/blob/59ad58ac/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/helper_test.py
----------------------------------------------------------------------
diff --git a/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/helper_test.py b/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/helper_test.py
deleted file mode 100644
index 6fa3eed..0000000
--- a/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/helper_test.py
+++ /dev/null
@@ -1,265 +0,0 @@
-#
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements. See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-"""Tests for datastore helper."""
-import sys
-import unittest
-
-from mock import MagicMock
-
-from apache_beam.io.google_cloud_platform.datastore.v1 import fake_datastore
-from apache_beam.io.google_cloud_platform.datastore.v1 import helper
-from apache_beam.tests.test_utils import patch_retry
-
-
-# Protect against environments where apitools library is not available.
-# pylint: disable=wrong-import-order, wrong-import-position
-try:
- from google.cloud.proto.datastore.v1 import datastore_pb2
- from google.cloud.proto.datastore.v1 import entity_pb2
- from google.cloud.proto.datastore.v1 import query_pb2
- from google.cloud.proto.datastore.v1.entity_pb2 import Key
- from googledatastore.connection import RPCError
- from googledatastore import helper as datastore_helper
-except ImportError:
- datastore_helper = None
-# pylint: enable=wrong-import-order, wrong-import-position
-
-
-@unittest.skipIf(datastore_helper is None, 'GCP dependencies are not installed')
-class HelperTest(unittest.TestCase):
-
- def setUp(self):
- self._mock_datastore = MagicMock()
- self._query = query_pb2.Query()
- self._query.kind.add().name = 'dummy_kind'
- patch_retry(self, helper)
-
- def permanent_datastore_failure(self, req):
- raise RPCError("dummy", 500, "failed")
-
- def transient_datastore_failure(self, req):
- if self._transient_fail_count:
- self._transient_fail_count -= 1
- raise RPCError("dummy", 500, "failed")
- else:
- return datastore_pb2.RunQueryResponse()
-
- def test_query_iterator(self):
- self._mock_datastore.run_query.side_effect = (
- self.permanent_datastore_failure)
- query_iterator = helper.QueryIterator("project", None, self._query,
- self._mock_datastore)
- self.assertRaises(RPCError, iter(query_iterator).next)
- self.assertEqual(6, len(self._mock_datastore.run_query.call_args_list))
-
- def test_query_iterator_with_transient_failures(self):
- self._mock_datastore.run_query.side_effect = (
- self.transient_datastore_failure)
- query_iterator = helper.QueryIterator("project", None, self._query,
- self._mock_datastore)
- fail_count = 2
- self._transient_fail_count = fail_count
- for _ in query_iterator:
- pass
-
- self.assertEqual(fail_count + 1,
- len(self._mock_datastore.run_query.call_args_list))
-
- def test_query_iterator_with_single_batch(self):
- num_entities = 100
- batch_size = 500
- self.check_query_iterator(num_entities, batch_size, self._query)
-
- def test_query_iterator_with_multiple_batches(self):
- num_entities = 1098
- batch_size = 500
- self.check_query_iterator(num_entities, batch_size, self._query)
-
- def test_query_iterator_with_exact_batch_multiple(self):
- num_entities = 1000
- batch_size = 500
- self.check_query_iterator(num_entities, batch_size, self._query)
-
- def test_query_iterator_with_query_limit(self):
- num_entities = 1098
- batch_size = 500
- self._query.limit.value = 1004
- self.check_query_iterator(num_entities, batch_size, self._query)
-
- def test_query_iterator_with_large_query_limit(self):
- num_entities = 1098
- batch_size = 500
- self._query.limit.value = 10000
- self.check_query_iterator(num_entities, batch_size, self._query)
-
- def check_query_iterator(self, num_entities, batch_size, query):
- """A helper method to test the QueryIterator.
-
- Args:
- num_entities: number of entities contained in the fake datastore.
- batch_size: the number of entities returned by fake datastore in one req.
- query: the query to be executed
-
- """
- entities = fake_datastore.create_entities(num_entities)
- self._mock_datastore.run_query.side_effect = \
- fake_datastore.create_run_query(entities, batch_size)
- query_iterator = helper.QueryIterator("project", None, self._query,
- self._mock_datastore)
-
- i = 0
- for entity in query_iterator:
- self.assertEqual(entity, entities[i].entity)
- i += 1
-
- limit = query.limit.value if query.HasField('limit') else sys.maxint
- self.assertEqual(i, min(num_entities, limit))
-
- def test_is_key_valid(self):
- key = entity_pb2.Key()
- # Complete with name, no ancestor
- datastore_helper.add_key_path(key, 'kind', 'name')
- self.assertTrue(helper.is_key_valid(key))
-
- key = entity_pb2.Key()
- # Complete with id, no ancestor
- datastore_helper.add_key_path(key, 'kind', 12)
- self.assertTrue(helper.is_key_valid(key))
-
- key = entity_pb2.Key()
- # Incomplete, no ancestor
- datastore_helper.add_key_path(key, 'kind')
- self.assertFalse(helper.is_key_valid(key))
-
- key = entity_pb2.Key()
- # Complete with name and ancestor
- datastore_helper.add_key_path(key, 'kind', 'name', 'kind2', 'name2')
- self.assertTrue(helper.is_key_valid(key))
-
- key = entity_pb2.Key()
- # Complete with id and ancestor
- datastore_helper.add_key_path(key, 'kind', 'name', 'kind2', 123)
- self.assertTrue(helper.is_key_valid(key))
-
- key = entity_pb2.Key()
- # Incomplete with ancestor
- datastore_helper.add_key_path(key, 'kind', 'name', 'kind2')
- self.assertFalse(helper.is_key_valid(key))
-
- key = entity_pb2.Key()
- self.assertFalse(helper.is_key_valid(key))
-
- def test_compare_path_with_different_kind(self):
- p1 = Key.PathElement()
- p1.kind = 'dummy1'
-
- p2 = Key.PathElement()
- p2.kind = 'dummy2'
-
- self.assertLess(helper.compare_path(p1, p2), 0)
-
- def test_compare_path_with_different_id(self):
- p1 = Key.PathElement()
- p1.kind = 'dummy'
- p1.id = 10
-
- p2 = Key.PathElement()
- p2.kind = 'dummy'
- p2.id = 15
-
- self.assertLess(helper.compare_path(p1, p2), 0)
-
- def test_compare_path_with_different_name(self):
- p1 = Key.PathElement()
- p1.kind = 'dummy'
- p1.name = "dummy1"
-
- p2 = Key.PathElement()
- p2.kind = 'dummy'
- p2.name = 'dummy2'
-
- self.assertLess(helper.compare_path(p1, p2), 0)
-
- def test_compare_path_of_different_type(self):
- p1 = Key.PathElement()
- p1.kind = 'dummy'
- p1.id = 10
-
- p2 = Key.PathElement()
- p2.kind = 'dummy'
- p2.name = 'dummy'
-
- self.assertLess(helper.compare_path(p1, p2), 0)
-
- def test_key_comparator_with_different_partition(self):
- k1 = Key()
- k1.partition_id.namespace_id = 'dummy1'
- k2 = Key()
- k2.partition_id.namespace_id = 'dummy2'
- self.assertRaises(ValueError, helper.key_comparator, k1, k2)
-
- def test_key_comparator_with_single_path(self):
- k1 = Key()
- k2 = Key()
- p1 = k1.path.add()
- p2 = k2.path.add()
- p1.kind = p2.kind = 'dummy'
- self.assertEqual(helper.key_comparator(k1, k2), 0)
-
- def test_key_comparator_with_multiple_paths_1(self):
- k1 = Key()
- k2 = Key()
- p11 = k1.path.add()
- p12 = k1.path.add()
- p21 = k2.path.add()
- p11.kind = p12.kind = p21.kind = 'dummy'
- self.assertGreater(helper.key_comparator(k1, k2), 0)
-
- def test_key_comparator_with_multiple_paths_2(self):
- k1 = Key()
- k2 = Key()
- p11 = k1.path.add()
- p21 = k2.path.add()
- p22 = k2.path.add()
- p11.kind = p21.kind = p22.kind = 'dummy'
- self.assertLess(helper.key_comparator(k1, k2), 0)
-
- def test_key_comparator_with_multiple_paths_3(self):
- k1 = Key()
- k2 = Key()
- p11 = k1.path.add()
- p12 = k1.path.add()
- p21 = k2.path.add()
- p22 = k2.path.add()
- p11.kind = p12.kind = p21.kind = p22.kind = 'dummy'
- self.assertEqual(helper.key_comparator(k1, k2), 0)
-
- def test_key_comparator_with_multiple_paths_4(self):
- k1 = Key()
- k2 = Key()
- p11 = k1.path.add()
- p12 = k2.path.add()
- p21 = k2.path.add()
- p11.kind = p12.kind = 'dummy'
- # make path2 greater than path1
- p21.kind = 'dummy1'
- self.assertLess(helper.key_comparator(k1, k2), 0)
-
-
-if __name__ == '__main__':
- unittest.main()
http://git-wip-us.apache.org/repos/asf/beam/blob/59ad58ac/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/query_splitter.py
----------------------------------------------------------------------
diff --git a/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/query_splitter.py b/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/query_splitter.py
deleted file mode 100644
index 71590d3..0000000
--- a/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/query_splitter.py
+++ /dev/null
@@ -1,275 +0,0 @@
-#
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements. See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-"""Implements a Cloud Datastore query splitter."""
-
-from apache_beam.io.google_cloud_platform.datastore.v1 import helper
-
-# Protect against environments where datastore library is not available.
-# pylint: disable=wrong-import-order, wrong-import-position
-try:
- from google.cloud.proto.datastore.v1 import datastore_pb2
- from google.cloud.proto.datastore.v1 import query_pb2
- from google.cloud.proto.datastore.v1.query_pb2 import PropertyFilter
- from google.cloud.proto.datastore.v1.query_pb2 import CompositeFilter
- from googledatastore import helper as datastore_helper
- UNSUPPORTED_OPERATORS = [PropertyFilter.LESS_THAN,
- PropertyFilter.LESS_THAN_OR_EQUAL,
- PropertyFilter.GREATER_THAN,
- PropertyFilter.GREATER_THAN_OR_EQUAL]
-except ImportError:
- UNSUPPORTED_OPERATORS = None
-# pylint: enable=wrong-import-order, wrong-import-position
-
-
-__all__ = [
- 'get_splits',
-]
-
-SCATTER_PROPERTY_NAME = '__scatter__'
-KEY_PROPERTY_NAME = '__key__'
-# The number of keys to sample for each split.
-KEYS_PER_SPLIT = 32
-
-
-def get_splits(datastore, query, num_splits, partition=None):
- """Returns a list of sharded queries for the given Cloud Datastore query.
-
- This will create up to the desired number of splits, however it may return
- less splits if the desired number of splits is unavailable. This will happen
- if the number of split points provided by the underlying Datastore is less
- than the desired number, which will occur if the number of results for the
- query is too small.
-
- This implementation of the QuerySplitter uses the __scatter__ property to
- gather random split points for a query.
-
- Note: This implementation is derived from the java query splitter in
- https://github.com/GoogleCloudPlatform/google-cloud-datastore/blob/master/java/datastore/src/main/java/com/google/datastore/v1/client/QuerySplitterImpl.java
-
- Args:
- datastore: the datastore client.
- query: the query to split.
- num_splits: the desired number of splits.
- partition: the partition the query is running in.
-
- Returns:
- A list of split queries, of a max length of `num_splits`
- """
-
- # Validate that the number of splits is not out of bounds.
- if num_splits < 1:
- raise ValueError('The number of splits must be greater than 0.')
-
- if num_splits == 1:
- return [query]
-
- _validate_query(query)
-
- splits = []
- scatter_keys = _get_scatter_keys(datastore, query, num_splits, partition)
- last_key = None
- for next_key in _get_split_key(scatter_keys, num_splits):
- splits.append(_create_split(last_key, next_key, query))
- last_key = next_key
-
- splits.append(_create_split(last_key, None, query))
- return splits
-
-
-def _validate_query(query):
- """ Verifies that the given query can be properly scattered."""
-
- if len(query.kind) != 1:
- raise ValueError('Query must have exactly one kind.')
-
- if len(query.order) != 0:
- raise ValueError('Query cannot have any sort orders.')
-
- if query.HasField('limit'):
- raise ValueError('Query cannot have a limit set.')
-
- if query.offset > 0:
- raise ValueError('Query cannot have an offset set.')
-
- _validate_filter(query.filter)
-
-
-def _validate_filter(filter):
- """Validates that we only have allowable filters.
-
- Note that equality and ancestor filters are allowed, however they may result
- in inefficient sharding.
- """
-
- if filter.HasField('composite_filter'):
- for sub_filter in filter.composite_filter.filters:
- _validate_filter(sub_filter)
- elif filter.HasField('property_filter'):
- if filter.property_filter.op in UNSUPPORTED_OPERATORS:
- raise ValueError('Query cannot have any inequality filters.')
- else:
- pass
-
-
-def _create_scatter_query(query, num_splits):
- """Creates a scatter query from the given user query."""
-
- scatter_query = query_pb2.Query()
- for kind in query.kind:
- scatter_kind = scatter_query.kind.add()
- scatter_kind.CopyFrom(kind)
-
- # ascending order
- datastore_helper.add_property_orders(scatter_query, SCATTER_PROPERTY_NAME)
-
- # There is a split containing entities before and after each scatter entity:
- # ||---*------*------*------*------*------*------*---|| * = scatter entity
- # If we represent each split as a region before a scatter entity, there is an
- # extra region following the last scatter point. Thus, we do not need the
- # scatter entity for the last region.
- scatter_query.limit.value = (num_splits - 1) * KEYS_PER_SPLIT
- datastore_helper.add_projection(scatter_query, KEY_PROPERTY_NAME)
-
- return scatter_query
-
-
-def _get_scatter_keys(datastore, query, num_splits, partition):
- """Gets a list of split keys given a desired number of splits.
-
- This list will contain multiple split keys for each split. Only a single split
- key will be chosen as the split point, however providing multiple keys allows
- for more uniform sharding.
-
- Args:
- numSplits: the number of desired splits.
- query: the user query.
- partition: the partition to run the query in.
- datastore: the client to datastore containing the data.
-
- Returns:
- A list of scatter keys returned by Datastore.
- """
- scatter_point_query = _create_scatter_query(query, num_splits)
-
- key_splits = []
- while True:
- req = datastore_pb2.RunQueryRequest()
- if partition:
- req.partition_id.CopyFrom(partition)
-
- req.query.CopyFrom(scatter_point_query)
-
- resp = datastore.run_query(req)
- for entity_result in resp.batch.entity_results:
- key_splits.append(entity_result.entity.key)
-
- if resp.batch.more_results != query_pb2.QueryResultBatch.NOT_FINISHED:
- break
-
- scatter_point_query.start_cursor = resp.batch.end_cursor
- scatter_point_query.limit.value -= len(resp.batch.entity_results)
-
- key_splits.sort(helper.key_comparator)
- return key_splits
-
-
-def _get_split_key(keys, num_splits):
- """Given a list of keys and a number of splits find the keys to split on.
-
- Args:
- keys: the list of keys.
- num_splits: the number of splits.
-
- Returns:
- A list of keys to split on.
-
- """
-
- # If the number of keys is less than the number of splits, we are limited
- # in the number of splits we can make.
- if not keys or (len(keys) < (num_splits - 1)):
- return keys
-
- # Calculate the number of keys per split. This should be KEYS_PER_SPLIT,
- # but may be less if there are not KEYS_PER_SPLIT * (numSplits - 1) scatter
- # entities.
- #
- # Consider the following dataset, where - represents an entity and
- # * represents an entity that is returned as a scatter entity:
- # ||---*-----*----*-----*-----*------*----*----||
- # If we want 4 splits in this data, the optimal split would look like:
- # ||---*-----*----*-----*-----*------*----*----||
- # | | |
- # The scatter keys in the last region are not useful to us, so we never
- # request them:
- # ||---*-----*----*-----*-----*------*---------||
- # | | |
- # With 6 scatter keys we want to set scatter points at indexes: 1, 3, 5.
- #
- # We keep this as a float so that any "fractional" keys per split get
- # distributed throughout the splits and don't make the last split
- # significantly larger than the rest.
-
- num_keys_per_split = max(1.0, float(len(keys)) / (num_splits - 1))
-
- split_keys = []
-
- # Grab the last sample for each split, otherwise the first split will be too
- # small.
- for i in range(1, num_splits):
- split_index = int(round(i * num_keys_per_split) - 1)
- split_keys.append(keys[split_index])
-
- return split_keys
-
-
-def _create_split(last_key, next_key, query):
- """Create a new {@link Query} given the query and range..
-
- Args:
- last_key: the previous key. If null then assumed to be the beginning.
- next_key: the next key. If null then assumed to be the end.
- query: the desired query.
-
- Returns:
- A split query with fetches entities in the range [last_key, next_key)
- """
- if not (last_key or next_key):
- return query
-
- split_query = query_pb2.Query()
- split_query.CopyFrom(query)
- composite_filter = split_query.filter.composite_filter
- composite_filter.op = CompositeFilter.AND
-
- if query.HasField('filter'):
- composite_filter.filters.add().CopyFrom(query.filter)
-
- if last_key:
- lower_bound = composite_filter.filters.add()
- lower_bound.property_filter.property.name = KEY_PROPERTY_NAME
- lower_bound.property_filter.op = PropertyFilter.GREATER_THAN_OR_EQUAL
- lower_bound.property_filter.value.key_value.CopyFrom(last_key)
-
- if next_key:
- upper_bound = composite_filter.filters.add()
- upper_bound.property_filter.property.name = KEY_PROPERTY_NAME
- upper_bound.property_filter.op = PropertyFilter.LESS_THAN
- upper_bound.property_filter.value.key_value.CopyFrom(next_key)
-
- return split_query
http://git-wip-us.apache.org/repos/asf/beam/blob/59ad58ac/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/query_splitter_test.py
----------------------------------------------------------------------
diff --git a/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/query_splitter_test.py b/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/query_splitter_test.py
deleted file mode 100644
index 9e87dec..0000000
--- a/sdks/python/apache_beam/io/google_cloud_platform/datastore/v1/query_splitter_test.py
+++ /dev/null
@@ -1,208 +0,0 @@
-#
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements. See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-"""Cloud Datastore query splitter test."""
-
-import unittest
-
-from mock import MagicMock
-from mock import call
-
-from apache_beam.io.google_cloud_platform.datastore.v1 import fake_datastore
-from apache_beam.io.google_cloud_platform.datastore.v1 import query_splitter
-
-# Protect against environments where datastore library is not available.
-# pylint: disable=wrong-import-order, wrong-import-position
-try:
- from google.cloud.proto.datastore.v1 import datastore_pb2
- from google.cloud.proto.datastore.v1 import query_pb2
- from google.cloud.proto.datastore.v1.query_pb2 import PropertyFilter
-except ImportError:
- datastore_pb2 = None
-# pylint: enable=wrong-import-order, wrong-import-position
-
-
-@unittest.skipIf(datastore_pb2 is None, 'GCP dependencies are not installed')
-class QuerySplitterTest(unittest.TestCase):
-
- def test_get_splits_query_with_multiple_kinds(self):
- query = query_pb2.Query()
- query.kind.add()
- query.kind.add()
- self.assertRaises(ValueError, query_splitter.get_splits, None, query, 4)
-
- def test_get_splits_query_with_order(self):
- query = query_pb2.Query()
- query.kind.add()
- query.order.add()
-
- self.assertRaises(ValueError, query_splitter.get_splits, None, query, 3)
-
- def test_get_splits_query_with_unsupported_filter(self):
- query = query_pb2.Query()
- query.kind.add()
- test_filter = query.filter.composite_filter.filters.add()
- test_filter.property_filter.op = PropertyFilter.GREATER_THAN
- self.assertRaises(ValueError, query_splitter.get_splits, None, query, 2)
-
- def test_get_splits_query_with_limit(self):
- query = query_pb2.Query()
- query.kind.add()
- query.limit.value = 10
- self.assertRaises(ValueError, query_splitter.get_splits, None, query, 2)
-
- def test_get_splits_query_with_offset(self):
- query = query_pb2.Query()
- query.kind.add()
- query.offset = 10
- self.assertRaises(ValueError, query_splitter.get_splits, None, query, 2)
-
- def test_create_scatter_query(self):
- query = query_pb2.Query()
- kind = query.kind.add()
- kind.name = 'shakespeare-demo'
- num_splits = 10
- scatter_query = query_splitter._create_scatter_query(query, num_splits)
- self.assertEqual(scatter_query.kind[0], kind)
- self.assertEqual(scatter_query.limit.value,
- (num_splits -1) * query_splitter.KEYS_PER_SPLIT)
- self.assertEqual(scatter_query.order[0].direction,
- query_pb2.PropertyOrder.ASCENDING)
- self.assertEqual(scatter_query.projection[0].property.name,
- query_splitter.KEY_PROPERTY_NAME)
-
- def test_get_splits_with_two_splits(self):
- query = query_pb2.Query()
- kind = query.kind.add()
- kind.name = 'shakespeare-demo'
- num_splits = 2
- num_entities = 97
- batch_size = 9
-
- self.check_get_splits(query, num_splits, num_entities, batch_size)
-
- def test_get_splits_with_multiple_splits(self):
- query = query_pb2.Query()
- kind = query.kind.add()
- kind.name = 'shakespeare-demo'
- num_splits = 4
- num_entities = 369
- batch_size = 12
-
- self.check_get_splits(query, num_splits, num_entities, batch_size)
-
- def test_get_splits_with_large_num_splits(self):
- query = query_pb2.Query()
- kind = query.kind.add()
- kind.name = 'shakespeare-demo'
- num_splits = 10
- num_entities = 4
- batch_size = 10
-
- self.check_get_splits(query, num_splits, num_entities, batch_size)
-
- def test_get_splits_with_small_num_entities(self):
- query = query_pb2.Query()
- kind = query.kind.add()
- kind.name = 'shakespeare-demo'
- num_splits = 4
- num_entities = 50
- batch_size = 10
-
- self.check_get_splits(query, num_splits, num_entities, batch_size)
-
- def test_get_splits_with_batch_size_exact_multiple(self):
- """Test get_splits when num scatter keys is a multiple of batch size."""
- query = query_pb2.Query()
- kind = query.kind.add()
- kind.name = 'shakespeare-demo'
- num_splits = 4
- num_entities = 400
- batch_size = 32
-
- self.check_get_splits(query, num_splits, num_entities, batch_size)
-
- def test_get_splits_with_large_batch_size(self):
- """Test get_splits when all scatter keys are retured in a single req."""
- query = query_pb2.Query()
- kind = query.kind.add()
- kind.name = 'shakespeare-demo'
- num_splits = 4
- num_entities = 400
- batch_size = 500
-
- self.check_get_splits(query, num_splits, num_entities, batch_size)
-
- def check_get_splits(self, query, num_splits, num_entities, batch_size):
- """A helper method to test the query_splitter get_splits method.
-
- Args:
- query: the query to be split
- num_splits: number of splits
- num_entities: number of scatter entities contained in the fake datastore.
- batch_size: the number of entities returned by fake datastore in one req.
- """
-
- entities = fake_datastore.create_entities(num_entities)
- mock_datastore = MagicMock()
- # Assign a fake run_query method as a side_effect to the mock.
- mock_datastore.run_query.side_effect = \
- fake_datastore.create_run_query(entities, batch_size)
-
- split_queries = query_splitter.get_splits(mock_datastore, query, num_splits)
-
- # if request num_splits is greater than num_entities, the best it can
- # do is one entity per split.
- expected_num_splits = min(num_splits, num_entities + 1)
- self.assertEqual(len(split_queries), expected_num_splits)
-
- expected_requests = QuerySplitterTest.create_scatter_requests(
- query, num_splits, batch_size, num_entities)
-
- expected_calls = []
- for req in expected_requests:
- expected_calls.append(call(req))
-
- self.assertEqual(expected_calls, mock_datastore.run_query.call_args_list)
-
- @staticmethod
- def create_scatter_requests(query, num_splits, batch_size, num_entities):
- """Creates a list of expected scatter requests from the query splitter.
-
- This list of requests returned is used to verify that the query splitter
- made the same number of requests in the same order to datastore.
- """
-
- requests = []
- count = (num_splits - 1) * query_splitter.KEYS_PER_SPLIT
- start_cursor = ''
- i = 0
- scatter_query = query_splitter._create_scatter_query(query, count)
- while i < count and i < num_entities:
- request = datastore_pb2.RunQueryRequest()
- request.query.CopyFrom(scatter_query)
- request.query.start_cursor = start_cursor
- request.query.limit.value = count - i
- requests.append(request)
- i += batch_size
- start_cursor = str(i)
-
- return requests
-
-
-if __name__ == '__main__':
- unittest.main()