#!/usr/bin/env python
# -*- encoding: utf-8 -*-
# Copyright 2011-2016, Nigel Small
#
# Licensed 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.
from py2neo.database.cypher import cypher_escape
def _property_equality_conditions(properties, offset=1):
for i, (key, value) in enumerate(properties.items(), start=offset):
if key == "__id__":
condition = "id(_)"
else:
condition = "_.%s" % cypher_escape(key)
if isinstance(value, (tuple, set, frozenset)):
condition += " IN {%d}" % i
parameters = {"%d" % i: list(value)}
else:
condition += " = {%d}" % i
parameters = {"%d" % i: value}
yield condition, parameters
[docs]
class NodeSelection(object):
""" An immutable set of node selection criteria.
"""
def __init__(self, graph, labels=frozenset(), conditions=tuple(), order_by=tuple(), skip=None, limit=None):
self.graph = graph
self._labels = frozenset(labels)
self._conditions = tuple(conditions)
self._order_by = tuple(order_by)
self._skip = skip
self._limit = limit
def __iter__(self):
for node, in self.graph.run(*self._query_and_parameters):
yield node
[docs]
def first(self):
""" Evaluate the selection and return the first
:py:class:`.Node` selected or :py:const:`None` if no matching
nodes are found.
:return: a single matching :py:class:`.Node` or :py:const:`None`
"""
return self.graph.evaluate(*self._query_and_parameters)
@property
def _query_and_parameters(self):
""" A tuple of the Cypher query and parameters used to select
the nodes that match the criteria for this selection.
:return: Cypher query string
"""
clauses = ["MATCH (_%s)" % "".join(":%s" % cypher_escape(label) for label in self._labels)]
parameters = {}
if self._conditions:
conditions = []
for condition in self._conditions:
if isinstance(condition, tuple):
condition, param = condition
parameters.update(param)
conditions.append(condition)
clauses.append("WHERE %s" % " AND ".join(conditions))
clauses.append("RETURN _")
if self._order_by:
clauses.append("ORDER BY %s" % (", ".join(self._order_by)))
if self._skip:
clauses.append("SKIP %d" % self._skip)
if self._limit is not None:
clauses.append("LIMIT %d" % self._limit)
return " ".join(clauses), parameters
[docs]
def where(self, *conditions, **properties):
""" Create a new selection based on this selection. The
criteria specified for refining the selection consist of
conditions and properties. Conditions are individual Cypher
expressions that would be found in a `WHERE` clause; properties
are used as exact matches for property values.
To refer to the current node within a condition expression, use
the underscore character ``_``. For example::
selection.where("_.name =~ 'J.*")
Simple property equalities can also be specified::
selection.where(born=1976)
:param conditions: Cypher expressions to add to the selection
`WHERE` clause
:param properties: exact property match keys and values
:return: refined selection object
"""
return self.__class__(self.graph, self._labels,
self._conditions + conditions + tuple(_property_equality_conditions(properties)),
self._order_by, self._skip, self._limit)
[docs]
def order_by(self, *fields):
""" Order by the fields or field expressions specified.
To refer to the current node within a field or field expression,
use the underscore character ``_``. For example::
selection.order_by("_.name", "max(_.a, _.b)")
:param fields: fields or field expressions to order by
:return: refined selection object
"""
return self.__class__(self.graph, self._labels, self._conditions,
fields, self._skip, self._limit)
[docs]
def skip(self, amount):
""" Skip the first `amount` nodes in the result.
:param amount: number of nodes to skip
:return: refined selection object
"""
return self.__class__(self.graph, self._labels, self._conditions,
self._order_by, amount, self._limit)
[docs]
def limit(self, amount):
""" Limit the selection to at most `amount` nodes.
:param amount: maximum number of nodes to select
:return: refined selection object
"""
return self.__class__(self.graph, self._labels, self._conditions,
self._order_by, self._skip, amount)
[docs]
class NodeSelector(object):
""" A :py:class:`.NodeSelector` can be used to locate nodes that
fulfil a specific set of criteria. Typically, a single node can be
identified passing a specific label and property key-value pair.
However, any number of labels and any condition supported by the
Cypher `WHERE` clause is allowed.
For a simple selection by label and property::
>>> from py2neo import Graph, NodeSelector
>>> graph = Graph()
>>> selector = NodeSelector(graph)
>>> selected = selector.select("Person", name="Keanu Reeves")
>>> list(selected)
[(f9726ea:Person {born:1964,name:"Keanu Reeves"})]
For a more comprehensive selection using Cypher expressions, the
:meth:`.NodeSelection.where` method can be used for further
refinement. Here, the underscore character can be used to refer to
the node being filtered::
>>> selected = selector.select("Person").where("_.name =~ 'J.*'", "1960 <= _.born < 1970")
>>> list(selected)
[(a03f6eb:Person {born:1967,name:"James Marshall"}),
(e59993d:Person {born:1966,name:"John Cusack"}),
(c44901e:Person {born:1960,name:"John Goodman"}),
(b141775:Person {born:1965,name:"John C. Reilly"}),
(e40244b:Person {born:1967,name:"Julia Roberts"})]
The underlying query is only evaluated when the selection undergoes
iteration or when a specific evaluation method is called (such as
:meth:`.NodeSelection.first`). This means that a :class:`.NodeSelection`
instance may be reused before and after a data changes for different
results.
"""
_selection_class = NodeSelection
def __init__(self, graph):
self.graph = graph
self._all = self._selection_class(self.graph)
[docs]
def select(self, *labels, **properties):
""" Describe a basic node selection using labels and property equality.
:param labels: node labels to match
:param properties: set of property keys and values to match
:return: :py:class:`.NodeSelection` instance
"""
if labels or properties:
return self._selection_class(self.graph, frozenset(labels),
tuple(_property_equality_conditions(properties)))
else:
return self._all