Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gcloud/datastore/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def __init__(self, dataset=None, kind=None, exclude_from_indexes=()):
# _implicit_environ._DatastoreBase to avoid split MRO.
self._dataset = dataset or _implicit_environ.DATASET
if kind:
self._key = Key(path=[{'kind': kind}])
self._key = Key(kind)
else:
self._key = None
self._exclude_from_indexes = set(exclude_from_indexes)
Expand Down
14 changes: 5 additions & 9 deletions gcloud/datastore/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,19 +65,15 @@ def key_from_protobuf(pb):
:rtype: :class:`gcloud.datastore.key.Key`
:returns: a new `Key` instance
"""
path = []
path_args = []
for element in pb.path_element:
element_dict = {'kind': element.kind}

path_args.append(element.kind)
if element.HasField('id'):
element_dict['id'] = element.id

path_args.append(element.id)
# This is safe: we expect proto objects returned will only have
# one of `name` or `id` set.
if element.HasField('name'):
element_dict['name'] = element.name

path.append(element_dict)
path_args.append(element.name)

dataset_id = None
if pb.partition_id.HasField('dataset_id'):
Expand All @@ -86,7 +82,7 @@ def key_from_protobuf(pb):
if pb.partition_id.HasField('namespace'):
namespace = pb.partition_id.namespace

return Key(path, namespace, dataset_id)
return Key(*path_args, namespace=namespace, dataset_id=dataset_id)


def _pb_attr_value(val):
Expand Down
117 changes: 95 additions & 22 deletions gcloud/datastore/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,51 @@
"""Create / interact with gcloud datastore keys."""

import copy
from itertools import izip
import six

from gcloud.datastore import datastore_v1_pb2 as datastore_pb


class Key(object):
"""An immutable representation of a datastore Key.

To create a basic key:

>>> Key('EntityKind', 1234)
<Key[{'kind': 'EntityKind', 'id': 1234}]>
>>> Key('EntityKind', 'foo')
<Key[{'kind': 'EntityKind', 'name': 'foo'}]>

To create a key with a parent:

>>> Key('Parent', 'foo', 'Child', 1234)
<Key[{'kind': 'Parent', 'name': 'foo'}, {'kind': 'Child', 'id': 1234}]>

To create a paritial key:

>>> Key('Parent', 'foo', 'Child')
<Key[{'kind': 'Parent', 'name': 'foo'}, {'kind': 'Child'}]>

.. automethod:: __init__
"""

def __init__(self, path=None, namespace=None, dataset_id=None):
def __init__(self, *path_args, **kwargs):
"""Constructor / initializer for a key.

:type namespace: :class:`str`
:param namespace: A namespace identifier for the key.
:type path_args: tuple of strings and ints
:param path_args: May represent a partial (odd length) or full (even
length) key path.

:type path: sequence of dicts
:param path: Each dict must have keys 'kind' (a string) and optionally
'name' (a string) or 'id' (an integer).
:type namespace: :class:`str`
:param namespace: A namespace identifier for the key. Can only be
passed as a keyword argument.

:type dataset_id: string
:param dataset: The dataset ID assigned by back-end for the key.
:param dataset_id: The dataset ID associated with the key. Can only be
passed as a keyword argument.

# This note will be obsolete by the end of #451.

.. note::
The key's ``_dataset_id`` field must be None for keys created
Expand All @@ -46,10 +69,51 @@ def __init__(self, path=None, namespace=None, dataset_id=None):
returned from the datastore backend. The application
**must** treat any value set by the back-end as opaque.
"""
self._path = path or [{'kind': ''}]
self._path = self._parse_path(path_args)
self._flat_path = path_args
self._parent = None
self._namespace = namespace
self._dataset_id = dataset_id
self._namespace = kwargs.get('namespace')
self._dataset_id = kwargs.get('dataset_id')

@staticmethod
def _parse_path(path_args):
"""Parses positional arguments into key path with kinds and IDs.

:rtype: list of dict
:returns: A list of key parts with kind and id or name set.
:raises: `ValueError` if there are no `path_args`, if one of the
kinds is not a string or if one of the IDs/names is not
a string or an integer.
"""
if len(path_args) == 0:
raise ValueError('Key path must not be empty.')

kind_list = path_args[::2]
id_or_name_list = path_args[1::2]
# Dummy sentinel value to pad incomplete key to even length path.
partial_ending = object()
if len(path_args) % 2 == 1:
id_or_name_list += (partial_ending,)

result = []
for kind, id_or_name in izip(kind_list, id_or_name_list):
curr_key_part = {}
if isinstance(kind, six.string_types):
curr_key_part['kind'] = kind
else:
raise ValueError(kind, 'Kind was not a string.')

if isinstance(id_or_name, six.string_types):
curr_key_part['name'] = id_or_name
elif isinstance(id_or_name, six.integer_types):
curr_key_part['id'] = id_or_name
elif id_or_name is not partial_ending:
raise ValueError(id_or_name,
'ID/name was not a string or integer.')

result.append(curr_key_part)

return result

def _clone(self):
"""Duplicates the Key.
Expand All @@ -74,8 +138,8 @@ def to_protobuf(self):
if self.dataset_id is not None:
key.partition_id.dataset_id = self.dataset_id

if self._namespace:
key.partition_id.namespace = self._namespace
if self.namespace:
key.partition_id.namespace = self.namespace

for item in self.path:
element = key.path_element.add()
Expand Down Expand Up @@ -118,15 +182,23 @@ def path(self):
"""
return copy.deepcopy(self._path)

@property
def flat_path(self):
"""Getter for the key path as a tuple.

:rtype: :class:`tuple` of string and int
:returns: The tuple of elements in the path.
"""
return self._flat_path

@property
def kind(self):
"""Kind getter. Based on the last element of path.

:rtype: :class:`str`
:returns: The kind of the current key.
"""
if self.path:
return self.path[-1].get('kind')
return self.path[-1]['kind']

@property
def id(self):
Expand All @@ -135,8 +207,7 @@ def id(self):
:rtype: :class:`int`
:returns: The (integer) ID of the key.
"""
if self.path:
return self.path[-1].get('id')
return self.path[-1].get('id')

@property
def name(self):
Expand All @@ -145,8 +216,7 @@ def name(self):
:rtype: :class:`str`
:returns: The (string) name of the key.
"""
if self.path:
return self.path[-1].get('name')
return self.path[-1].get('name')

@property
def id_or_name(self):
Expand Down Expand Up @@ -178,14 +248,17 @@ def _make_parent(self):
element of self's path. If self has only one path element,
returns None.
"""
parent_path = self.path[:-1]
if parent_path:
return Key(path=parent_path, dataset_id=self.dataset_id,
if self.is_partial:
parent_args = self.flat_path[:-1]
else:
parent_args = self.flat_path[:-2]
if parent_args:
return Key(*parent_args, dataset_id=self.dataset_id,
namespace=self.namespace)

@property
def parent(self):
"""Getter: return a new key for the next highest element in path.
"""The parent of the current key.

:rtype: :class:`gcloud.datastore.key.Key` or `NoneType`
:returns: a new `Key` instance, whose path consists of all but the last
Expand Down
2 changes: 1 addition & 1 deletion gcloud/datastore/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ def ancestor(self, ancestor):
This will return a clone of the current :class:`Query` filtered
by the ancestor provided. For example::

>>> parent_key = Key(path=[{'kind': 'Person', 'name': '1'}])
>>> parent_key = Key('Person', '1')
>>> query = dataset.query('Person')
>>> filtered_query = query.ancestor(parent_key)

Expand Down
2 changes: 1 addition & 1 deletion gcloud/datastore/test___init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,9 @@ def test_allocate_ids(self):
from gcloud._testing import _Monkey

CUSTOM_DATASET = _Dataset()
INCOMPLETE_KEY = Key()
NUM_IDS = 2
with _Monkey(_implicit_environ, DATASET=CUSTOM_DATASET):
INCOMPLETE_KEY = Key('KIND')
result = gcloud.datastore.allocate_ids(INCOMPLETE_KEY, NUM_IDS)

# Check the IDs returned.
Expand Down
Loading