def __init__(self, graph, hydrate=True): self.graph = graph # TODO: make function for subresource pattern below self._batch = Resource(graph.resource.metadata["batch"]) self._cypher = Resource(graph.resource.metadata["cypher"]) self.clear() self.hydrate = hydrate
def __init__(self, uri): self._begin = Resource(uri) self._begin_commit = Resource(uri + "/commit") self._execute = None self._commit = None self._statements = [] self._finished = False
def __init__(self, graph): Bindable.__init__(self, graph.service_root.uri.resolve("load2neo")) try: self.__load2neo_version = self.resource.metadata[ "load2neo_version"] except ClientError: raise NotImplementedError("Load2neo extension not available") self.__geoff_loader = Resource(self.resource.metadata["geoff_loader"])
def __init__(self, uri=None): if uri is None: service_root = ServiceRoot() manager = Resource(service_root.resource.metadata["management"]) monitor = Monitor(manager.metadata["services"]["monitor"]) uri = monitor.resource.uri Bindable.__init__(self, uri)
def __init__(self, graph): Bindable.__init__(self, graph.service_root.uri.resolve("load2neo")) try: self.__load2neo_version = self.resource.metadata["load2neo_version"] except ClientError: raise NotImplementedError("Load2neo extension not available") self.__geoff_loader = Resource(self.resource.metadata["geoff_loader"])
class Loader(Bindable): def __init__(self, graph): Bindable.__init__(self, graph.service_root.uri.resolve("load2neo")) try: self.__load2neo_version = self.resource.metadata[ "load2neo_version"] except ClientError: raise NotImplementedError("Load2neo extension not available") self.__geoff_loader = Resource(self.resource.metadata["geoff_loader"]) @property def load2neo_version(self): return self.__load2neo_version def load_geoff(self, geoff): """ Load Geoff data via the load2neo extension. >>> from py2neo import Graph >>> from py2neo.ext.geoff import Loader >>> graph = Graph() >>> loader = Loader(graph) >>> loader.load_geoff("(alice)<-[:KNOWS]->(bob)") [{'alice': (N1), 'bob': (N2)}] :param geoff: geoff data to load :return: list of node mappings """ return [{ key: self.graph.node(value) for key, value in json.loads(line).items() } for line in self.__geoff_loader.post(geoff).content.splitlines( keepends=False)]
class Loader(Bindable): def __init__(self, graph): Bindable.__init__(self, graph.service_root.uri.resolve("load2neo")) try: self.__load2neo_version = self.resource.metadata["load2neo_version"] except ClientError: raise NotImplementedError("Load2neo extension not available") self.__geoff_loader = Resource(self.resource.metadata["geoff_loader"]) @property def load2neo_version(self): return self.__load2neo_version def load_geoff(self, geoff): """ Load Geoff data via the load2neo extension. >>> from py2neo import Graph >>> from py2neo.ext.geoff import Loader >>> graph = Graph() >>> loader = Loader(graph) >>> loader.load_geoff("(alice)<-[:KNOWS]->(bob)") [{'alice': (N1), 'bob': (N2)}] :param geoff: geoff data to load :return: list of node mappings """ return [ {key: self.graph.node(value) for key, value in json.loads(line).items()} for line in self.__geoff_loader.post(geoff).content.splitlines(keepends=False) ]
def create(self, abstract): """ Create a node or relationship based on the abstract entity provided. For example:: batch = WriteBatch(graph) a = batch.create(node(name="Alice")) b = batch.create(node(name="Bob")) batch.create(rel(a, "KNOWS", b)) results = batch.submit() :param abstract: node or relationship :type abstract: abstract :return: batch request object """ entity = _cast(abstract, abstract=True) if isinstance(entity, Node): uri = self._uri_for(Resource(self.graph.resource.metadata["node"])) body = entity.properties elif isinstance(entity, Relationship): uri = self._uri_for(entity.start_node, "relationships") body = {"type": entity.type, "to": self._uri_for(entity.end_node)} if entity.properties: body["data"] = entity.properties else: raise TypeError(entity) return self.append_post(uri, body)
def __init__(self, content_type, uri, name=None): Bindable.__init__(self) self._content_type = content_type key_value_pos = uri.find("/{key}/{value}") if key_value_pos >= 0: self._searcher = ResourceTemplate(uri) self.bind(uri[:key_value_pos]) else: self.bind(uri) self._searcher = ResourceTemplate(uri.string + "/{key}/{value}") uri = self.resource.uri if self.graph.neo4j_version >= (1, 9): self._create_or_fail = Resource(uri.resolve("?uniqueness=create_or_fail")) self._get_or_create = Resource(uri.resolve("?uniqueness=get_or_create")) else: self._create_or_fail = None self._get_or_create = Resource(uri.resolve("?unique")) self._query_template = ResourceTemplate(uri.string + "{?query,order}") self._name = name or uri.path.segments[-1] self.__searcher_stem_cache = {}
def _index_manager(self, content_type): """ Fetch the index management resource for the given `content_type`. :param content_type: :return: """ if content_type is Node: uri = self.resource.metadata["node_index"] elif content_type is Relationship: uri = self.resource.metadata["relationship_index"] else: raise TypeError("Indexes can manage either Nodes or Relationships") return Resource(uri)
def __init__(self, content_type, uri, name=None): Bindable.__init__(self) self._content_type = content_type key_value_pos = uri.find("/{key}/{value}") if key_value_pos >= 0: self._searcher = ResourceTemplate(uri) self.bind(uri[:key_value_pos]) else: self.bind(uri) self._searcher = ResourceTemplate(uri.string + "/{key}/{value}") uri = self.resource.uri if self.graph.neo4j_version >= (1, 9): self._create_or_fail = Resource( uri.resolve("?uniqueness=create_or_fail")) self._get_or_create = Resource( uri.resolve("?uniqueness=get_or_create")) else: self._create_or_fail = None self._get_or_create = Resource(uri.resolve("?unique")) self._query_template = ResourceTemplate(uri.string + "{?query,order}") self._name = name or uri.path.segments[-1] self.__searcher_stem_cache = {}
def _post(self, resource): self._assert_unfinished() rs = resource.post({"statements": self._statements}) location = dict(rs.headers).get("location") if location: self._execute = Resource(location) j = rs.content rs.close() self._clear() if "commit" in j: self._commit = Resource(j["commit"]) if "errors" in j: errors = j["errors"] if len(errors) >= 1: error = errors[0] raise TransactionError.new(error["code"], error["message"]) out = [] for result in j["results"]: producer = RecordProducer(result["columns"]) out.append([ producer.produce(self._begin.service_root.graph.hydrate(r["rest"])) for r in result["data"] ]) return out
def fetch_latest_stats(self): """ Fetch the latest server statistics as a list of 2-tuples, each holding a `datetime` object and a named tuple of node, relationship and property counts. """ counts = namedtuple( "Stats", ("node_count", "relationship_count", "property_count")) uri = self.resource.metadata["resources"]["latest_data"] latest_data = Resource(uri).get().content timestamps = latest_data["timestamps"] data = latest_data["data"] data = zip( (datetime.fromtimestamp(t) for t in timestamps), (counts(*x) for x in zip( (numberise(n) for n in data["node_count"]), (numberise(n) for n in data["relationship_count"]), (numberise(n) for n in data["property_count"]), )), ) return data
class BatchRequestList(object): def __init__(self, graph, hydrate=True): self.graph = graph # TODO: make function for subresource pattern below self._batch = Resource(graph.resource.metadata["batch"]) self._cypher = Resource(graph.resource.metadata["cypher"]) self.clear() self.hydrate = hydrate def __len__(self): return len(self._requests) def __nonzero__(self): return bool(self._requests) def append(self, request): self._requests.append(request) return request def append_get(self, uri): return self.append(BatchRequest("GET", uri)) def append_put(self, uri, body=None): return self.append(BatchRequest("PUT", uri, body)) def append_post(self, uri, body=None): return self.append(BatchRequest("POST", uri, body)) def append_delete(self, uri): return self.append(BatchRequest("DELETE", uri)) def append_cypher(self, query, params=None): """ Append a Cypher query to this batch. Resources returned from Cypher queries cannot be referenced by other batch requests. :param query: Cypher query :type query: :py:class:`str` :param params: query parameters :type params: :py:class:`dict` :return: batch request object :rtype: :py:class:`_Batch.Request` """ if params: body = {"query": str(query), "params": dict(params)} else: body = {"query": str(query)} return self.append_post(self._uri_for(self._cypher), body) @property def _body(self): return [ { "id": i, "method": request.method, "to": str(request.uri), "body": request.body, } for i, request in enumerate(self._requests) ] def clear(self): """ Clear all requests from this batch. """ self._requests = [] def find(self, request): """ Find the position of a request within this batch. """ for i, req in pendulate(self._requests): if req == request: return i raise ValueError("Request not found") # TODO merge with Graph.relative_uri def _uri_for(self, resource, *segments, **kwargs): """ Return a relative URI in string format for the entity specified plus extra path segments. """ if isinstance(resource, int): uri = "{{{0}}}".format(resource) elif isinstance(resource, NodePointer): uri = "{{{0}}}".format(resource.address) elif isinstance(resource, BatchRequest): uri = "{{{0}}}".format(self.find(resource)) elif isinstance(resource, Node): # TODO: remove when Rel is also Bindable offset = len(resource.graph.resource.uri.string) uri = resource.resource.uri.string[offset:] else: offset = len(resource.service_root.graph.resource.uri) uri = str(resource.uri)[offset:] if segments: if not uri.endswith("/"): uri += "/" uri += "/".join(map(percent_encode, segments)) query = kwargs.get("query") if query is not None: uri += "?" + query return uri def _execute(self): request_count = len(self) request_text = "request" if request_count == 1 else "requests" batch_log.info("Executing batch with {0} {1}".format(request_count, request_text)) if __debug__: for id_, request in enumerate(self._requests): batch_log.debug(">>> {{{0}}} {1} {2} {3}".format(id_, request.method, request.uri, request.body)) try: response = self._batch.post(self._body) except (ClientError, ServerError) as e: if e.exception: # A CustomBatchError is a dynamically created subclass of # BatchError with the same name as the underlying server # exception CustomBatchError = type(str(e.exception), (BatchError,), {}) raise CustomBatchError(e) else: raise BatchError(e) else: return response def run(self): """ Execute the batch on the server and discard the results. If the batch results are not required, this will generally be the fastest execution method. """ return self._execute().close() def stream(self): """ Execute the batch on the server and return iterable results. This method allows handling of results as they are received from the server. :return: iterable results :rtype: :py:class:`BatchResponseList` """ response_list = BatchResponseList(self.graph, self._execute(), hydrate=self.hydrate) for response in response_list: yield response.body response_list.close() def submit(self): """ Execute the batch on the server and return a list of results. This method blocks until all results are received. :return: result records :rtype: :py:class:`list` """ response_list = BatchResponseList(self.graph, self._execute(), hydrate=self.hydrate) return [response.body for response in response_list.responses]
class BatchRequestList(object): def __init__(self, graph, hydrate=True): self.graph = graph # TODO: make function for subresource pattern below self._batch = Resource(graph.resource.metadata["batch"]) self._cypher = Resource(graph.resource.metadata["cypher"]) self.clear() self.hydrate = hydrate def __len__(self): return len(self._requests) def __nonzero__(self): return bool(self._requests) def append(self, request): self._requests.append(request) return request def append_get(self, uri): return self.append(BatchRequest("GET", uri)) def append_put(self, uri, body=None): return self.append(BatchRequest("PUT", uri, body)) def append_post(self, uri, body=None): return self.append(BatchRequest("POST", uri, body)) def append_delete(self, uri): return self.append(BatchRequest("DELETE", uri)) def append_cypher(self, query, params=None): """ Append a Cypher query to this batch. Resources returned from Cypher queries cannot be referenced by other batch requests. :param query: Cypher query :type query: :py:class:`str` :param params: query parameters :type params: :py:class:`dict` :return: batch request object :rtype: :py:class:`_Batch.Request` """ if params: body = {"query": str(query), "params": dict(params)} else: body = {"query": str(query)} return self.append_post(self._uri_for(self._cypher), body) @property def _body(self): return [{ "id": i, "method": request.method, "to": str(request.uri), "body": request.body, } for i, request in enumerate(self._requests)] def clear(self): """ Clear all requests from this batch. """ self._requests = [] def find(self, request): """ Find the position of a request within this batch. """ for i, req in pendulate(self._requests): if req == request: return i raise ValueError("Request not found") # TODO merge with Graph.relative_uri def _uri_for(self, resource, *segments, **kwargs): """ Return a relative URI in string format for the entity specified plus extra path segments. """ if isinstance(resource, int): uri = "{{{0}}}".format(resource) elif isinstance(resource, NodePointer): uri = "{{{0}}}".format(resource.address) elif isinstance(resource, BatchRequest): uri = "{{{0}}}".format(self.find(resource)) elif isinstance(resource, Node): # TODO: remove when Rel is also Bindable offset = len(resource.graph.resource.uri.string) uri = resource.resource.uri.string[offset:] else: offset = len(resource.service_root.graph.resource.uri) uri = str(resource.uri)[offset:] if segments: if not uri.endswith("/"): uri += "/" uri += "/".join(map(percent_encode, segments)) query = kwargs.get("query") if query is not None: uri += "?" + query return uri def _execute(self): request_count = len(self) request_text = "request" if request_count == 1 else "requests" batch_log.info("Executing batch with {0} {1}".format( request_count, request_text)) if __debug__: for id_, request in enumerate(self._requests): batch_log.debug(">>> {{{0}}} {1} {2} {3}".format( id_, request.method, request.uri, request.body)) try: response = self._batch.post(self._body) except (ClientError, ServerError) as e: if e.exception: # A CustomBatchError is a dynamically created subclass of # BatchError with the same name as the underlying server # exception CustomBatchError = type(str(e.exception), (BatchError, ), {}) raise CustomBatchError(e) else: raise BatchError(e) else: return response def run(self): """ Execute the batch on the server and discard the results. If the batch results are not required, this will generally be the fastest execution method. """ return self._execute().close() def stream(self): """ Execute the batch on the server and return iterable results. This method allows handling of results as they are received from the server. :return: iterable results :rtype: :py:class:`BatchResponseList` """ response_list = BatchResponseList(self.graph, self._execute(), hydrate=self.hydrate) for response in response_list: yield response.body response_list.close() def submit(self): """ Execute the batch on the server and return a list of results. This method blocks until all results are received. :return: result records :rtype: :py:class:`list` """ response_list = BatchResponseList(self.graph, self._execute(), hydrate=self.hydrate) return [response.body for response in response_list.responses]
class Index(Bindable): """ Searchable database index which can contain either nodes or relationships. .. seealso:: :py:func:`Graph.get_or_create_index` """ __instances = {} def __new__(cls, content_type, uri, name=None): """ Fetch a cached instance if one is available, otherwise create, cache and return a new instance. :param uri: URI of the cached resource :return: a resource instance """ inst = super(Index, cls).__new__(cls) return cls.__instances.setdefault(uri, inst) def __init__(self, content_type, uri, name=None): Bindable.__init__(self) self._content_type = content_type key_value_pos = uri.find("/{key}/{value}") if key_value_pos >= 0: self._searcher = ResourceTemplate(uri) self.bind(uri[:key_value_pos]) else: self.bind(uri) self._searcher = ResourceTemplate(uri.string + "/{key}/{value}") uri = self.resource.uri if self.graph.neo4j_version >= (1, 9): self._create_or_fail = Resource( uri.resolve("?uniqueness=create_or_fail")) self._get_or_create = Resource( uri.resolve("?uniqueness=get_or_create")) else: self._create_or_fail = None self._get_or_create = Resource(uri.resolve("?unique")) self._query_template = ResourceTemplate(uri.string + "{?query,order}") self._name = name or uri.path.segments[-1] self.__searcher_stem_cache = {} def __repr__(self): return "{0}({1}, {2})".format(self.__class__.__name__, self._content_type.__name__, repr(URI(self).string)) def _searcher_stem_for_key(self, key): if key not in self.__searcher_stem_cache: stem = self._searcher.uri_template.string.partition("{key}")[0] self.__searcher_stem_cache[key] = stem + percent_encode(key) + "/" return self.__searcher_stem_cache[key] def add(self, key, value, entity): """ Add an entity to this index under the `key`:`value` pair supplied:: # create a node and obtain a reference to the "People" node index alice, = graph.create({"name": "Alice Smith"}) people = graph.get_or_create_index(neo4j.Node, "People") # add the node to the index people.add("family_name", "Smith", alice) Note that while Neo4j indexes allow multiple entities to be added under a particular key:value, the same entity may only be represented once; this method is therefore idempotent. """ self.resource.post({ "key": key, "value": value, "uri": str(URI(entity)) }) return entity def add_if_none(self, key, value, entity): """ Add an entity to this index under the `key`:`value` pair supplied if no entry already exists at that point:: # obtain a reference to the "Rooms" node index and # add node `alice` to room 100 if empty rooms = graph.get_or_create_index(neo4j.Node, "Rooms") rooms.add_if_none("room", 100, alice) If added, this method returns the entity, otherwise :py:const:`None` is returned. """ rs = self._get_or_create.post({ "key": key, "value": value, "uri": str(URI(entity)) }) if rs.status_code == CREATED: return entity else: return None @property def content_type(self): """ Return the type of entity contained within this index. Will return either :py:class:`Node` or :py:class:`Relationship`. """ return self._content_type @property def name(self): """ Return the name of this index. """ return self._name def get(self, key, value): """ Fetch a list of all entities from the index which are associated with the `key`:`value` pair supplied:: # obtain a reference to the "People" node index and # get all nodes where `family_name` equals "Smith" people = graph.get_or_create_index(neo4j.Node, "People") smiths = people.get("family_name", "Smith") .. """ return [ self.graph.hydrate(assembled(result)) for i, result in grouped( self._searcher.expand(key=key, value=value).get()) ] def create(self, key, value, abstract): """ Create and index a new node or relationship using the abstract provided. """ batch = LegacyWriteBatch(self.graph) if self._content_type is Node: batch.create(abstract) batch.add_indexed_node(self, key, value, 0) elif self._content_type is Relationship: batch.create(abstract) batch.add_indexed_relationship(self, key, value, 0) else: raise TypeError(self._content_type) entity, index_entry = batch.submit() return entity def _create_unique(self, key, value, abstract): """ Internal method to support `get_or_create` and `create_if_none`. """ if self._content_type is Node: body = {"key": key, "value": value, "properties": abstract} elif self._content_type is Relationship: body = { "key": key, "value": value, "start": str(abstract[0].__uri__), "type": abstract[1], "end": str(abstract[2].__uri__), "properties": abstract[3] if len(abstract) > 3 else None } else: raise TypeError(self._content_type) return self._get_or_create.post(body) def get_or_create(self, key, value, abstract): """ Fetch a single entity from the index which is associated with the `key`:`value` pair supplied, creating a new entity with the supplied details if none exists:: # obtain a reference to the "Contacts" node index and # ensure that Alice exists therein contacts = graph.get_or_create_index(neo4j.Node, "Contacts") alice = contacts.get_or_create("name", "SMITH, Alice", { "given_name": "Alice Jane", "family_name": "Smith", "phone": "01234 567 890", "mobile": "07890 123 456" }) # obtain a reference to the "Friendships" relationship index and # ensure that Alice and Bob's friendship is registered (`alice` # and `bob` refer to existing nodes) friendships = graph.get_or_create_index(neo4j.Relationship, "Friendships") alice_and_bob = friendships.get_or_create( "friends", "Alice & Bob", (alice, "KNOWS", bob) ) .. """ return self.graph.hydrate( assembled(self._create_unique(key, value, abstract))) def create_if_none(self, key, value, abstract): """ Create a new entity with the specified details within the current index, under the `key`:`value` pair supplied, if no such entity already exists. If creation occurs, the new entity will be returned, otherwise :py:const:`None` will be returned:: # obtain a reference to the "Contacts" node index and # create a node for Alice if one does not already exist contacts = graph.get_or_create_index(neo4j.Node, "Contacts") alice = contacts.create_if_none("name", "SMITH, Alice", { "given_name": "Alice Jane", "family_name": "Smith", "phone": "01234 567 890", "mobile": "07890 123 456" }) .. """ rs = self._create_unique(key, value, abstract) if rs.status_code == CREATED: return self.graph.hydrate(assembled(rs)) else: return None def remove(self, key=None, value=None, entity=None): """ Remove any entries from the index which match the parameters supplied. The allowed parameter combinations are: `key`, `value`, `entity` remove a specific entity indexed under a given key-value pair `key`, `value` remove all entities indexed under a given key-value pair `key`, `entity` remove a specific entity indexed against a given key but with any value `entity` remove all occurrences of a specific entity regardless of key and value """ if key and value and entity: t = ResourceTemplate(self.resource.uri.string + "/{key}/{value}/{entity}") t.expand(key=key, value=value, entity=entity._id).delete() elif key and value: uris = [ URI(entity.resource.metadata["indexed"]) for entity in self.get(key, value) ] batch = LegacyWriteBatch(self.graph) for uri in uris: batch.append_delete(uri) batch.run() elif key and entity: t = ResourceTemplate(self.resource.uri.string + "/{key}/{entity}") t.expand(key=key, entity=entity._id).delete() elif entity: t = ResourceTemplate(self.resource.uri.string + "/{entity}") t.expand(entity=entity._id).delete() else: raise TypeError("Illegal parameter combination for index removal") def query(self, query): """ Query the index according to the supplied query criteria, returning a list of matched entities:: # obtain a reference to the "People" node index and # get all nodes where `family_name` equals "Smith" people = graph.get_or_create_index(neo4j.Node, "People") s_people = people.query("family_name:S*") The query syntax used should be appropriate for the configuration of the index being queried. For indexes with default configuration, this should be Apache Lucene query syntax. """ resource = self._query_template.expand(query=query) for i, result in grouped(resource.get()): yield self.graph.hydrate(assembled(result)) def _query_with_score(self, query, order): resource = self._query_template.expand(query=query, order=order) for i, result in grouped(resource.get()): meta = assembled(result) yield self.graph.hydrate(meta), meta["score"] def query_by_index(self, query): return self._query_with_score(query, "index") def query_by_relevance(self, query): return self._query_with_score(query, "relevance") def query_by_score(self, query): return self._query_with_score(query, "score")
class Transaction(object): """ A transaction is a transient resource that allows multiple Cypher statements to be executed within a single server transaction. """ def __init__(self, uri): self._begin = Resource(uri) self._begin_commit = Resource(uri + "/commit") self._execute = None self._commit = None self._statements = [] self._finished = False def _clear(self): self._statements = [] def _assert_unfinished(self): if self._finished: raise TransactionFinished() @property def finished(self): """ Indicates whether or not this transaction has been completed or is still open. :return: :py:const:`True` if this transaction has finished, :py:const:`False` otherwise """ return self._finished def append(self, statement, parameters=None): """ Append a statement to the current queue of statements to be executed. :param statement: the statement to execute :param parameters: a dictionary of execution parameters """ self._assert_unfinished() # OrderedDict is used here to avoid statement/parameters ordering bug self._statements.append(OrderedDict([ ("statement", statement), ("parameters", dict(parameters or {})), ("resultDataContents", ["REST"]), ])) def _post(self, resource): self._assert_unfinished() rs = resource.post({"statements": self._statements}) location = dict(rs.headers).get("location") if location: self._execute = Resource(location) j = rs.content rs.close() self._clear() if "commit" in j: self._commit = Resource(j["commit"]) if "errors" in j: errors = j["errors"] if len(errors) >= 1: error = errors[0] raise TransactionError.new(error["code"], error["message"]) out = [] for result in j["results"]: producer = RecordProducer(result["columns"]) out.append([ producer.produce(self._begin.service_root.graph.hydrate(r["rest"])) for r in result["data"] ]) return out def execute(self): """ Send all pending statements to the server for execution, leaving the transaction open for further statements. :return: list of results from pending statements """ return self._post(self._execute or self._begin) def commit(self): """ Send all pending statements to the server for execution and commit the transaction. :return: list of results from pending statements """ try: return self._post(self._commit or self._begin_commit) finally: self._finished = True def rollback(self): """ Rollback the current transaction. """ self._assert_unfinished() try: if self._execute: self._execute.delete() finally: self._finished = True
class Index(Bindable): """ Searchable database index which can contain either nodes or relationships. .. seealso:: :py:func:`Graph.get_or_create_index` """ __instances = {} def __new__(cls, content_type, uri, name=None): """ Fetch a cached instance if one is available, otherwise create, cache and return a new instance. :param uri: URI of the cached resource :return: a resource instance """ inst = super(Index, cls).__new__(cls) return cls.__instances.setdefault(uri, inst) def __init__(self, content_type, uri, name=None): Bindable.__init__(self) self._content_type = content_type key_value_pos = uri.find("/{key}/{value}") if key_value_pos >= 0: self._searcher = ResourceTemplate(uri) self.bind(uri[:key_value_pos]) else: self.bind(uri) self._searcher = ResourceTemplate(uri.string + "/{key}/{value}") uri = self.resource.uri if self.graph.neo4j_version >= (1, 9): self._create_or_fail = Resource(uri.resolve("?uniqueness=create_or_fail")) self._get_or_create = Resource(uri.resolve("?uniqueness=get_or_create")) else: self._create_or_fail = None self._get_or_create = Resource(uri.resolve("?unique")) self._query_template = ResourceTemplate(uri.string + "{?query,order}") self._name = name or uri.path.segments[-1] self.__searcher_stem_cache = {} def __repr__(self): return "{0}({1}, {2})".format( self.__class__.__name__, self._content_type.__name__, repr(URI(self).string) ) def _searcher_stem_for_key(self, key): if key not in self.__searcher_stem_cache: stem = self._searcher.uri_template.string.partition("{key}")[0] self.__searcher_stem_cache[key] = stem + percent_encode(key) + "/" return self.__searcher_stem_cache[key] def add(self, key, value, entity): """ Add an entity to this index under the `key`:`value` pair supplied:: # create a node and obtain a reference to the "People" node index alice, = graph.create({"name": "Alice Smith"}) people = graph.get_or_create_index(neo4j.Node, "People") # add the node to the index people.add("family_name", "Smith", alice) Note that while Neo4j indexes allow multiple entities to be added under a particular key:value, the same entity may only be represented once; this method is therefore idempotent. """ self.resource.post({ "key": key, "value": value, "uri": str(URI(entity)) }) return entity def add_if_none(self, key, value, entity): """ Add an entity to this index under the `key`:`value` pair supplied if no entry already exists at that point:: # obtain a reference to the "Rooms" node index and # add node `alice` to room 100 if empty rooms = graph.get_or_create_index(neo4j.Node, "Rooms") rooms.add_if_none("room", 100, alice) If added, this method returns the entity, otherwise :py:const:`None` is returned. """ rs = self._get_or_create.post({ "key": key, "value": value, "uri": str(URI(entity)) }) if rs.status_code == CREATED: return entity else: return None @property def content_type(self): """ Return the type of entity contained within this index. Will return either :py:class:`Node` or :py:class:`Relationship`. """ return self._content_type @property def name(self): """ Return the name of this index. """ return self._name def get(self, key, value): """ Fetch a list of all entities from the index which are associated with the `key`:`value` pair supplied:: # obtain a reference to the "People" node index and # get all nodes where `family_name` equals "Smith" people = graph.get_or_create_index(neo4j.Node, "People") smiths = people.get("family_name", "Smith") .. """ return [ self.graph.hydrate(assembled(result)) for i, result in grouped(self._searcher.expand(key=key, value=value).get()) ] def create(self, key, value, abstract): """ Create and index a new node or relationship using the abstract provided. """ batch = LegacyWriteBatch(self.graph) if self._content_type is Node: batch.create(abstract) batch.add_indexed_node(self, key, value, 0) elif self._content_type is Relationship: batch.create(abstract) batch.add_indexed_relationship(self, key, value, 0) else: raise TypeError(self._content_type) entity, index_entry = batch.submit() return entity def _create_unique(self, key, value, abstract): """ Internal method to support `get_or_create` and `create_if_none`. """ if self._content_type is Node: body = { "key": key, "value": value, "properties": abstract } elif self._content_type is Relationship: body = { "key": key, "value": value, "start": str(abstract[0].__uri__), "type": abstract[1], "end": str(abstract[2].__uri__), "properties": abstract[3] if len(abstract) > 3 else None } else: raise TypeError(self._content_type) return self._get_or_create.post(body) def get_or_create(self, key, value, abstract): """ Fetch a single entity from the index which is associated with the `key`:`value` pair supplied, creating a new entity with the supplied details if none exists:: # obtain a reference to the "Contacts" node index and # ensure that Alice exists therein contacts = graph.get_or_create_index(neo4j.Node, "Contacts") alice = contacts.get_or_create("name", "SMITH, Alice", { "given_name": "Alice Jane", "family_name": "Smith", "phone": "01234 567 890", "mobile": "07890 123 456" }) # obtain a reference to the "Friendships" relationship index and # ensure that Alice and Bob's friendship is registered (`alice` # and `bob` refer to existing nodes) friendships = graph.get_or_create_index(neo4j.Relationship, "Friendships") alice_and_bob = friendships.get_or_create( "friends", "Alice & Bob", (alice, "KNOWS", bob) ) .. """ return self.graph.hydrate(assembled(self._create_unique(key, value, abstract))) def create_if_none(self, key, value, abstract): """ Create a new entity with the specified details within the current index, under the `key`:`value` pair supplied, if no such entity already exists. If creation occurs, the new entity will be returned, otherwise :py:const:`None` will be returned:: # obtain a reference to the "Contacts" node index and # create a node for Alice if one does not already exist contacts = graph.get_or_create_index(neo4j.Node, "Contacts") alice = contacts.create_if_none("name", "SMITH, Alice", { "given_name": "Alice Jane", "family_name": "Smith", "phone": "01234 567 890", "mobile": "07890 123 456" }) .. """ rs = self._create_unique(key, value, abstract) if rs.status_code == CREATED: return self.graph.hydrate(assembled(rs)) else: return None def remove(self, key=None, value=None, entity=None): """ Remove any entries from the index which match the parameters supplied. The allowed parameter combinations are: `key`, `value`, `entity` remove a specific entity indexed under a given key-value pair `key`, `value` remove all entities indexed under a given key-value pair `key`, `entity` remove a specific entity indexed against a given key but with any value `entity` remove all occurrences of a specific entity regardless of key and value """ if key and value and entity: t = ResourceTemplate(self.resource.uri.string + "/{key}/{value}/{entity}") t.expand(key=key, value=value, entity=entity._id).delete() elif key and value: uris = [ URI(entity.resource.metadata["indexed"]) for entity in self.get(key, value) ] batch = LegacyWriteBatch(self.graph) for uri in uris: batch.append_delete(uri) batch.run() elif key and entity: t = ResourceTemplate(self.resource.uri.string + "/{key}/{entity}") t.expand(key=key, entity=entity._id).delete() elif entity: t = ResourceTemplate(self.resource.uri.string + "/{entity}") t.expand(entity=entity._id).delete() else: raise TypeError("Illegal parameter combination for index removal") def query(self, query): """ Query the index according to the supplied query criteria, returning a list of matched entities:: # obtain a reference to the "People" node index and # get all nodes where `family_name` equals "Smith" people = graph.get_or_create_index(neo4j.Node, "People") s_people = people.query("family_name:S*") The query syntax used should be appropriate for the configuration of the index being queried. For indexes with default configuration, this should be Apache Lucene query syntax. """ resource = self._query_template.expand(query=query) for i, result in grouped(resource.get()): yield self.graph.hydrate(assembled(result)) def _query_with_score(self, query, order): resource = self._query_template.expand(query=query, order=order) for i, result in grouped(resource.get()): meta = assembled(result) yield self.graph.hydrate(meta), meta["score"] def query_by_index(self, query): return self._query_with_score(query, "index") def query_by_relevance(self, query): return self._query_with_score(query, "relevance") def query_by_score(self, query): return self._query_with_score(query, "score")