1656 lines
47 KiB
Python
1656 lines
47 KiB
Python
|
# Copyright 2023 Memgraph Ltd.
|
|||
|
#
|
|||
|
# Use of this software is governed by the Business Source License
|
|||
|
# included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
|||
|
# License, and you may not use this file except in compliance with the Business Source License.
|
|||
|
#
|
|||
|
# As of the Change Date specified in that file, in accordance with
|
|||
|
# the Business Source License, use of this software will be governed
|
|||
|
# by the Apache License, Version 2.0, included in the file
|
|||
|
# licenses/APL.txt.
|
|||
|
|
|||
|
"""
|
|||
|
This module provides a mock Python API for easy development of custom openCypher procedures.
|
|||
|
The API's interface is fully consistent with the Python API in mgp.py. Because of that, you
|
|||
|
can develop procedures without setting Memgraph up for each run, and run them from Memgraph
|
|||
|
when they're implemented in full.
|
|||
|
"""
|
|||
|
|
|||
|
import datetime
|
|||
|
import inspect
|
|||
|
import sys
|
|||
|
import typing
|
|||
|
from collections import namedtuple
|
|||
|
from copy import deepcopy
|
|||
|
from functools import wraps
|
|||
|
|
|||
|
import _mgp_mock
|
|||
|
|
|||
|
|
|||
|
class InvalidContextError(Exception):
|
|||
|
"""
|
|||
|
Signals using a graph element instance outside of the registered procedure.
|
|||
|
"""
|
|||
|
|
|||
|
pass
|
|||
|
|
|||
|
|
|||
|
class UnknownError(Exception):
|
|||
|
"""
|
|||
|
Signals unspecified failure.
|
|||
|
"""
|
|||
|
|
|||
|
pass
|
|||
|
|
|||
|
|
|||
|
class LogicErrorError(Exception):
|
|||
|
"""
|
|||
|
Signals faulty logic within the program such as violating logical
|
|||
|
preconditions or class invariants and may be preventable.
|
|||
|
"""
|
|||
|
|
|||
|
pass
|
|||
|
|
|||
|
|
|||
|
class DeletedObjectError(Exception):
|
|||
|
"""
|
|||
|
Signals accessing an already deleted object.
|
|||
|
"""
|
|||
|
|
|||
|
pass
|
|||
|
|
|||
|
|
|||
|
class InvalidArgumentError(Exception):
|
|||
|
"""
|
|||
|
Signals that some of the arguments have invalid values.
|
|||
|
"""
|
|||
|
|
|||
|
pass
|
|||
|
|
|||
|
|
|||
|
class ImmutableObjectError(Exception):
|
|||
|
"""
|
|||
|
Signals modification of an immutable object.
|
|||
|
"""
|
|||
|
|
|||
|
pass
|
|||
|
|
|||
|
|
|||
|
class ValueConversionError(Exception):
|
|||
|
"""
|
|||
|
Signals that conversion between python and cypher values failed.
|
|||
|
"""
|
|||
|
|
|||
|
pass
|
|||
|
|
|||
|
|
|||
|
class Label:
|
|||
|
"""A vertex label."""
|
|||
|
|
|||
|
__slots__ = ("_name",)
|
|||
|
|
|||
|
def __init__(self, name: str):
|
|||
|
self._name = name
|
|||
|
|
|||
|
@property
|
|||
|
def name(self) -> str:
|
|||
|
"""
|
|||
|
Get the name of the label.
|
|||
|
|
|||
|
Returns:
|
|||
|
A string with the name of the label.
|
|||
|
|
|||
|
Example:
|
|||
|
```label.name```
|
|||
|
"""
|
|||
|
return self._name
|
|||
|
|
|||
|
def __eq__(self, other) -> bool:
|
|||
|
if isinstance(other, Label):
|
|||
|
return self._name == other.name
|
|||
|
|
|||
|
if isinstance(other, str):
|
|||
|
return self._name == other
|
|||
|
|
|||
|
return NotImplemented
|
|||
|
|
|||
|
|
|||
|
# Named property value of a Vertex or an Edge.
|
|||
|
# It would be better to use typing.NamedTuple with typed fields, but that is
|
|||
|
# not available in Python 3.5.
|
|||
|
Property = namedtuple("Property", ("name", "value"))
|
|||
|
|
|||
|
|
|||
|
class Properties:
|
|||
|
"""Collection of the properties of a vertex or an edge."""
|
|||
|
|
|||
|
__slots__ = ("_vertex_or_edge", "_len")
|
|||
|
|
|||
|
def __init__(self, vertex_or_edge):
|
|||
|
if not isinstance(vertex_or_edge, (_mgp_mock.Vertex, _mgp_mock.Edge)):
|
|||
|
raise TypeError(f"Expected _mgp_mock.Vertex or _mgp_mock.Edge, got {type(vertex_or_edge)}")
|
|||
|
|
|||
|
self._len = None
|
|||
|
self._vertex_or_edge = vertex_or_edge
|
|||
|
|
|||
|
def __deepcopy__(self, memo):
|
|||
|
# In line with the Python API, this is the same as the shallow copy.
|
|||
|
return Properties(self._vertex_or_edge)
|
|||
|
|
|||
|
def get(self, property_name: str, default=None) -> object:
|
|||
|
"""
|
|||
|
Get the value of the property with the given name, otherwise return the default value.
|
|||
|
|
|||
|
Args:
|
|||
|
property_name: String with the property name.
|
|||
|
default: The value to return if there is no `property_name` property.
|
|||
|
|
|||
|
Returns:
|
|||
|
The value associated with `property_name` or, if there’s no such property, the `default` argument.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If the edge or vertex is out of context.
|
|||
|
DeletedObjectError: If the edge has been deleted.
|
|||
|
|
|||
|
Examples:
|
|||
|
```
|
|||
|
vertex.properties.get(property_name)
|
|||
|
edge.properties.get(property_name)
|
|||
|
```
|
|||
|
"""
|
|||
|
if not self._vertex_or_edge.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
try:
|
|||
|
return self[property_name]
|
|||
|
except KeyError:
|
|||
|
return default
|
|||
|
|
|||
|
def set(self, property_name: str, value: object) -> None:
|
|||
|
"""
|
|||
|
Set the value of the given property. If `value` is `None`, the property is removed.
|
|||
|
|
|||
|
Args:
|
|||
|
property_name: String with the property name.
|
|||
|
value: The new value of the `property_name` property.
|
|||
|
|
|||
|
Raises:
|
|||
|
ImmutableObjectError: If the object is immutable.
|
|||
|
DeletedObjectError: If the edge has been deleted.
|
|||
|
ValueConversionError: If `value` is vertex, edge or path.
|
|||
|
|
|||
|
Examples:
|
|||
|
```
|
|||
|
vertex.properties.set(property_name, value)
|
|||
|
edge.properties.set(property_name, value)
|
|||
|
```
|
|||
|
"""
|
|||
|
if not self._vertex_or_edge.underlying_graph_is_mutable():
|
|||
|
raise ImmutableObjectError("Cannot modify immutable object.")
|
|||
|
|
|||
|
self[property_name] = value
|
|||
|
|
|||
|
def items(self) -> typing.Iterable[Property]:
|
|||
|
"""
|
|||
|
Iterate over the properties. Doesn’t return a dynamic view of the properties, but copies the
|
|||
|
current properties.
|
|||
|
|
|||
|
Returns:
|
|||
|
Iterable `Property` of names and values.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If the edge or vertex is out of context.
|
|||
|
DeletedObjectError: If the edge or vertex has been deleted.
|
|||
|
|
|||
|
Examples:
|
|||
|
```
|
|||
|
items = vertex.properties.items()
|
|||
|
for it in items:
|
|||
|
name = it.name
|
|||
|
value = it.value
|
|||
|
```
|
|||
|
```
|
|||
|
items = edge.properties.items()
|
|||
|
for it in items:
|
|||
|
name = it.name
|
|||
|
value = it.value
|
|||
|
```
|
|||
|
"""
|
|||
|
if not self._vertex_or_edge.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
if self._vertex_or_edge.is_deleted():
|
|||
|
raise DeletedObjectError("Accessing deleted object.")
|
|||
|
|
|||
|
vertex_or_edge_properties = self._vertex_or_edge.properties
|
|||
|
|
|||
|
for property in vertex_or_edge_properties:
|
|||
|
yield Property(*property)
|
|||
|
|
|||
|
def keys(self) -> typing.Iterable[str]:
|
|||
|
"""
|
|||
|
Iterate over property names. Doesn’t return a dynamic view of the property names, but copies the
|
|||
|
name of the current properties.
|
|||
|
|
|||
|
Returns:
|
|||
|
`Iterable` of strings that represent the property names (keys).
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If edge or vertex is out of context.
|
|||
|
DeletedObjectError: If the edge or vertex has been deleted.
|
|||
|
|
|||
|
Examples:
|
|||
|
```
|
|||
|
graph.vertex.properties.keys()
|
|||
|
graph.edge.properties.keys()
|
|||
|
```
|
|||
|
"""
|
|||
|
if not self._vertex_or_edge.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
for item in self.items():
|
|||
|
yield item.name
|
|||
|
|
|||
|
def values(self) -> typing.Iterable[object]:
|
|||
|
"""
|
|||
|
Iterate over property values. Doesn’t return a dynamic view of the property values, but copies the
|
|||
|
values of the current properties.
|
|||
|
|
|||
|
Returns:
|
|||
|
`Iterable` of property values.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If edge or vertex is out of context.
|
|||
|
DeletedObjectError: If the edge or vertex has been deleted.
|
|||
|
|
|||
|
Examples:
|
|||
|
```
|
|||
|
vertex.properties.values()
|
|||
|
edge.properties.values()
|
|||
|
```
|
|||
|
"""
|
|||
|
if not self._vertex_or_edge.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
for item in self.items():
|
|||
|
yield item.value
|
|||
|
|
|||
|
def __len__(self) -> int:
|
|||
|
"""
|
|||
|
Get the count of the vertex or edge’s properties.
|
|||
|
|
|||
|
Returns:
|
|||
|
The count of the stored properties.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If the edge or vertex is out of context.
|
|||
|
DeletedObjectError: If the edge or vertex has been deleted.
|
|||
|
|
|||
|
Examples:
|
|||
|
```
|
|||
|
len(vertex.properties)
|
|||
|
len(edge.properties)
|
|||
|
```
|
|||
|
"""
|
|||
|
if not self._vertex_or_edge.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
if self._len is None:
|
|||
|
self._len = sum(1 for _ in self.items())
|
|||
|
|
|||
|
return self._len
|
|||
|
|
|||
|
def __iter__(self) -> typing.Iterable[str]:
|
|||
|
"""
|
|||
|
Iterate over property names.
|
|||
|
|
|||
|
Returns:
|
|||
|
`Iterable` of strings that represent property names.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If edge or vertex is out of context.
|
|||
|
DeletedObjectError: If the edge or vertex has been deleted.
|
|||
|
|
|||
|
Examples:
|
|||
|
```
|
|||
|
iter(vertex.properties)
|
|||
|
iter(edge.properties)
|
|||
|
```
|
|||
|
"""
|
|||
|
if not self._vertex_or_edge.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
for item in self.items():
|
|||
|
yield item.name
|
|||
|
|
|||
|
def __getitem__(self, property_name: str) -> object:
|
|||
|
"""
|
|||
|
Get the value of the property with the given name, otherwise raise a KeyError.
|
|||
|
|
|||
|
Args:
|
|||
|
property_name: String with the property name.
|
|||
|
|
|||
|
Returns:
|
|||
|
Value of the named property.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If edge or vertex is out of context.
|
|||
|
DeletedObjectError: If the edge or vertex has been deleted.
|
|||
|
|
|||
|
Examples:
|
|||
|
```
|
|||
|
vertex.properties[property_name]
|
|||
|
edge.properties[property_name]
|
|||
|
```
|
|||
|
"""
|
|||
|
if not self._vertex_or_edge.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
if self._vertex_or_edge.is_deleted():
|
|||
|
raise DeletedObjectError("Accessing deleted object.")
|
|||
|
|
|||
|
return self._vertex_or_edge.get_property(property_name)
|
|||
|
|
|||
|
def __setitem__(self, property_name: str, value: object) -> None:
|
|||
|
"""
|
|||
|
Set the value of the given property. If `value` is `None`, the property
|
|||
|
is removed.
|
|||
|
|
|||
|
Args:
|
|||
|
property_name: String with the property name.
|
|||
|
value: Object that represents the value to be set.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If the edge or vertex is out of context.
|
|||
|
ImmutableObjectError: If the object is immutable.
|
|||
|
DeletedObjectError: If the edge or vertex has been deleted.
|
|||
|
ValueConversionError: If `value` is vertex, edge or path.
|
|||
|
|
|||
|
Examples:
|
|||
|
```
|
|||
|
vertex.properties[property_name] = value
|
|||
|
edge.properties[property_name] = value
|
|||
|
```
|
|||
|
"""
|
|||
|
if not self._vertex_or_edge.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
if not self._vertex_or_edge.underlying_graph_is_mutable():
|
|||
|
raise ImmutableObjectError("Cannot modify immutable object.")
|
|||
|
|
|||
|
if isinstance(value, (Vertex, Edge, Path)):
|
|||
|
raise ValueConversionError("Value conversion failed")
|
|||
|
|
|||
|
if self._vertex_or_edge.is_deleted():
|
|||
|
raise DeletedObjectError("Accessing deleted object.")
|
|||
|
|
|||
|
self._vertex_or_edge.set_property(property_name, value)
|
|||
|
|
|||
|
def __contains__(self, property_name: str) -> bool:
|
|||
|
"""
|
|||
|
Check if there is a property with the given name.
|
|||
|
|
|||
|
Args:
|
|||
|
property_name: String with the property name.
|
|||
|
|
|||
|
Returns:
|
|||
|
Boolean value that represents whether a property with the given name exists.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If edge or vertex is out of context.
|
|||
|
DeletedObjectError: If the edge or vertex has been deleted.
|
|||
|
|
|||
|
Examples:
|
|||
|
```
|
|||
|
if property_name in vertex.properties:
|
|||
|
```
|
|||
|
```
|
|||
|
if property_name in edge.properties:
|
|||
|
```
|
|||
|
"""
|
|||
|
if not self._vertex_or_edge.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
try:
|
|||
|
_ = self[property_name]
|
|||
|
return True
|
|||
|
except KeyError:
|
|||
|
return False
|
|||
|
|
|||
|
|
|||
|
class EdgeType:
|
|||
|
"""Type of an Edge."""
|
|||
|
|
|||
|
__slots__ = ("_name",)
|
|||
|
|
|||
|
def __init__(self, name):
|
|||
|
self._name = name
|
|||
|
|
|||
|
@property
|
|||
|
def name(self) -> str:
|
|||
|
"""
|
|||
|
Get the name of an EdgeType.
|
|||
|
|
|||
|
Returns:
|
|||
|
The string with the name of the EdgeType.
|
|||
|
|
|||
|
Example:
|
|||
|
```edge.type.name```
|
|||
|
"""
|
|||
|
return self._name
|
|||
|
|
|||
|
def __eq__(self, other) -> bool:
|
|||
|
if isinstance(other, EdgeType):
|
|||
|
return self.name == other.name
|
|||
|
|
|||
|
if isinstance(other, str):
|
|||
|
return self.name == other
|
|||
|
|
|||
|
return NotImplemented
|
|||
|
|
|||
|
|
|||
|
if sys.version_info >= (3, 5, 2):
|
|||
|
EdgeId = typing.NewType("EdgeId", int)
|
|||
|
else:
|
|||
|
EdgeId = int
|
|||
|
|
|||
|
|
|||
|
class Edge:
|
|||
|
"""Represents a graph edge.
|
|||
|
|
|||
|
Access to an Edge is only valid during a single execution of a procedure in
|
|||
|
a query. You should not globally store an instance of an Edge. Using an
|
|||
|
invalid Edge instance will raise an InvalidContextError.
|
|||
|
"""
|
|||
|
|
|||
|
__slots__ = ("_edge",)
|
|||
|
|
|||
|
def __init__(self, edge):
|
|||
|
if not isinstance(edge, _mgp_mock.Edge):
|
|||
|
raise TypeError(f"Expected '_mgp_mock.Edge', got '{type(edge)}'")
|
|||
|
|
|||
|
self._edge = edge
|
|||
|
|
|||
|
def __deepcopy__(self, memo):
|
|||
|
# In line with the Python API, this is the same as the shallow copy.
|
|||
|
return Edge(self._edge)
|
|||
|
|
|||
|
def is_valid(self) -> bool:
|
|||
|
"""
|
|||
|
Check if the `Edge` is in a valid context, i.e. if it may be used.
|
|||
|
|
|||
|
Returns:
|
|||
|
A `bool` value that represents whether the edge is in a valid context.
|
|||
|
|
|||
|
Examples:
|
|||
|
```edge.is_valid()```
|
|||
|
"""
|
|||
|
return self._edge.is_valid()
|
|||
|
|
|||
|
def underlying_graph_is_mutable(self) -> bool:
|
|||
|
"""
|
|||
|
Check if the underlying `Graph` is mutable.
|
|||
|
|
|||
|
Returns:
|
|||
|
A `bool` value that represents whether the graph is mutable.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If the context is not valid.
|
|||
|
|
|||
|
Examples:
|
|||
|
```edge.underlying_graph_is_mutable()```
|
|||
|
"""
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
return self._edge.underlying_graph_is_mutable()
|
|||
|
|
|||
|
@property
|
|||
|
def id(self) -> EdgeId:
|
|||
|
"""
|
|||
|
Get the ID of the edge.
|
|||
|
|
|||
|
Returns:
|
|||
|
An `EdgeId` representing the edge’s ID.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If edge is out of context.
|
|||
|
|
|||
|
Examples:
|
|||
|
```edge.id```
|
|||
|
"""
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
return self._edge.id
|
|||
|
|
|||
|
@property
|
|||
|
def type(self) -> EdgeType:
|
|||
|
"""
|
|||
|
Get the type of the `Edge`.
|
|||
|
|
|||
|
Returns:
|
|||
|
`EdgeType` representing the edge’s type.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If edge is out of context.
|
|||
|
|
|||
|
Examples:
|
|||
|
```edge.type```
|
|||
|
"""
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
return EdgeType(self._edge.get_type_name())
|
|||
|
|
|||
|
@property
|
|||
|
def from_vertex(self) -> "Vertex":
|
|||
|
"""
|
|||
|
Get the source (tail) vertex of the edge.
|
|||
|
|
|||
|
Returns:
|
|||
|
`Vertex` that is the edge’s source/tail.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If edge is out of context.
|
|||
|
|
|||
|
Examples:
|
|||
|
```edge.from_vertex```
|
|||
|
"""
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
return Vertex(self._edge.from_vertex())
|
|||
|
|
|||
|
@property
|
|||
|
def to_vertex(self) -> "Vertex":
|
|||
|
"""
|
|||
|
Get the destination (head) vertex of the edge.
|
|||
|
|
|||
|
Returns:
|
|||
|
`Vertex` that is the edge’s destination/head.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If edge is out of context.
|
|||
|
|
|||
|
Examples:
|
|||
|
```edge.to_vertex```
|
|||
|
"""
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
return Vertex(self._edge.to_vertex())
|
|||
|
|
|||
|
@property
|
|||
|
def properties(self) -> Properties:
|
|||
|
"""
|
|||
|
Get the edge’s properties.
|
|||
|
|
|||
|
Returns:
|
|||
|
`Properties` containing all properties of the edge.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If edge is out of context.
|
|||
|
|
|||
|
Examples:
|
|||
|
```edge.properties```
|
|||
|
"""
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
return Properties(self._edge)
|
|||
|
|
|||
|
def __eq__(self, other) -> bool:
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
if not isinstance(other, Edge):
|
|||
|
return NotImplemented
|
|||
|
|
|||
|
return self.id == other.id
|
|||
|
|
|||
|
def __hash__(self) -> int:
|
|||
|
return hash(self.id)
|
|||
|
|
|||
|
|
|||
|
if sys.version_info >= (3, 5, 2):
|
|||
|
VertexId = typing.NewType("VertexId", int)
|
|||
|
else:
|
|||
|
VertexId = int
|
|||
|
|
|||
|
|
|||
|
class Vertex:
|
|||
|
"""Represents a graph vertex.
|
|||
|
|
|||
|
Access to a Vertex is only valid during a single execution of a procedure
|
|||
|
in a query. You should not globally store an instance of a Vertex. Using an
|
|||
|
invalid Vertex instance will raise an InvalidContextError.
|
|||
|
"""
|
|||
|
|
|||
|
__slots__ = ("_vertex",)
|
|||
|
|
|||
|
def __init__(self, vertex):
|
|||
|
if not isinstance(vertex, _mgp_mock.Vertex):
|
|||
|
raise TypeError(f"Expected '_mgp_mock.Vertex', got '{type(vertex)}'")
|
|||
|
|
|||
|
self._vertex = vertex
|
|||
|
|
|||
|
def __deepcopy__(self, memo):
|
|||
|
# In line with the Python API, this is the same as the shallow copy.
|
|||
|
return Vertex(self._vertex)
|
|||
|
|
|||
|
def is_valid(self) -> bool:
|
|||
|
"""
|
|||
|
Check if the vertex is in a valid context, i.e. if it may be used.
|
|||
|
|
|||
|
Returns:
|
|||
|
A `bool` value that represents whether the vertex is in a valid context.
|
|||
|
|
|||
|
Examples:
|
|||
|
```vertex.is_valid()```
|
|||
|
"""
|
|||
|
return self._vertex.is_valid()
|
|||
|
|
|||
|
def underlying_graph_is_mutable(self) -> bool:
|
|||
|
"""
|
|||
|
Check if the underlying graph is mutable.
|
|||
|
|
|||
|
Returns:
|
|||
|
A `bool` value that represents whether the graph is mutable.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If the context is not valid.
|
|||
|
|
|||
|
Examples:
|
|||
|
```edge.underlying_graph_is_mutable()```
|
|||
|
"""
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
return self._vertex.underlying_graph_is_mutable()
|
|||
|
|
|||
|
@property
|
|||
|
def id(self) -> VertexId:
|
|||
|
"""
|
|||
|
Get the ID of the vertex.
|
|||
|
|
|||
|
Returns:
|
|||
|
A `VertexId` representing the vertex’s ID.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If vertex is out of context.
|
|||
|
|
|||
|
Examples:
|
|||
|
```vertex.id```
|
|||
|
"""
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
return self._vertex.id
|
|||
|
|
|||
|
@property
|
|||
|
def labels(self) -> typing.Tuple[Label]:
|
|||
|
"""
|
|||
|
Get the labels of the vertex.
|
|||
|
|
|||
|
Returns:
|
|||
|
A tuple of `Label` instances representing individual labels.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If vertex is out of context.
|
|||
|
DeletedObjectError: If `Vertex` has been deleted.
|
|||
|
|
|||
|
Examples:
|
|||
|
```vertex.labels```
|
|||
|
"""
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
if self._vertex.is_deleted():
|
|||
|
raise DeletedObjectError("Accessing deleted object.")
|
|||
|
|
|||
|
return tuple(Label(label) for label in self._vertex.labels)
|
|||
|
|
|||
|
def add_label(self, label: str) -> None:
|
|||
|
"""
|
|||
|
Add the given label to the vertex.
|
|||
|
|
|||
|
Args:
|
|||
|
label: The label (`str`) to be added.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If `Vertex` is out of context.
|
|||
|
ImmutableObjectError: If `Vertex` is immutable.
|
|||
|
DeletedObjectError: If `Vertex` has been deleted.
|
|||
|
|
|||
|
Examples:
|
|||
|
```vertex.add_label(label)```
|
|||
|
"""
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
if self._vertex.is_deleted():
|
|||
|
raise DeletedObjectError("Accessing deleted object.")
|
|||
|
|
|||
|
return self._vertex.add_label(label)
|
|||
|
|
|||
|
def remove_label(self, label: str) -> None:
|
|||
|
"""
|
|||
|
Remove the given label from the vertex.
|
|||
|
|
|||
|
Args:
|
|||
|
label: The label (`str`) to be removed.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If `Vertex` is out of context.
|
|||
|
ImmutableObjectError: If `Vertex` is immutable.
|
|||
|
DeletedObjectError: If `Vertex` has been deleted.
|
|||
|
|
|||
|
Examples:
|
|||
|
```vertex.remove_label(label)```
|
|||
|
"""
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
if self._vertex.is_deleted():
|
|||
|
raise DeletedObjectError("Accessing deleted object.")
|
|||
|
|
|||
|
return self._vertex.remove_label(label)
|
|||
|
|
|||
|
@property
|
|||
|
def properties(self) -> Properties:
|
|||
|
"""
|
|||
|
Get the properties of the vertex.
|
|||
|
|
|||
|
Returns:
|
|||
|
The `Properties` of the vertex.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If `Vertex` is out of context.
|
|||
|
|
|||
|
Examples:
|
|||
|
```vertex.properties```
|
|||
|
"""
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
return Properties(self._vertex)
|
|||
|
|
|||
|
@property
|
|||
|
def in_edges(self) -> typing.Iterable[Edge]:
|
|||
|
"""
|
|||
|
Iterate over the inbound edges of the vertex.
|
|||
|
Doesn’t return a dynamic view of the edges, but copies the current inbound edges.
|
|||
|
|
|||
|
Returns:
|
|||
|
An `Iterable` of all `Edge` objects directed towards the vertex.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If `Vertex` is out of context.
|
|||
|
DeletedObjectError: If `Vertex` has been deleted.
|
|||
|
|
|||
|
Examples:
|
|||
|
```for edge in vertex.in_edges:```
|
|||
|
"""
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
if self._vertex.is_deleted():
|
|||
|
raise DeletedObjectError("Accessing deleted object.")
|
|||
|
|
|||
|
for edge in self._vertex.in_edges:
|
|||
|
yield Edge(edge)
|
|||
|
|
|||
|
@property
|
|||
|
def out_edges(self) -> typing.Iterable[Edge]:
|
|||
|
"""
|
|||
|
Iterate over the outbound edges of the vertex.
|
|||
|
Doesn’t return a dynamic view of the edges, but copies the current outbound edges.
|
|||
|
|
|||
|
Returns:
|
|||
|
An `Iterable` of all `Edge` objects directed outwards from the vertex.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If `Vertex` is out of context.
|
|||
|
DeletedObjectError: If `Vertex` has been deleted.
|
|||
|
|
|||
|
Examples:
|
|||
|
```for edge in vertex.in_edges:```
|
|||
|
"""
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
if self._vertex.is_deleted():
|
|||
|
raise DeletedObjectError("Accessing deleted object.")
|
|||
|
|
|||
|
for edge in self._vertex.out_edges:
|
|||
|
yield Edge(edge)
|
|||
|
|
|||
|
def __eq__(self, other) -> bool:
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
if not isinstance(other, Vertex):
|
|||
|
return NotImplemented
|
|||
|
|
|||
|
return self.id == other.id
|
|||
|
|
|||
|
def __hash__(self) -> int:
|
|||
|
return hash(self.id)
|
|||
|
|
|||
|
|
|||
|
class Path:
|
|||
|
"""Represents a path comprised of `Vertex` and `Edge` instances."""
|
|||
|
|
|||
|
__slots__ = ("_path", "_vertices", "_edges")
|
|||
|
|
|||
|
def __init__(self, starting_vertex_or_path: typing.Union[_mgp_mock.Path, Vertex]):
|
|||
|
"""Initialize with a starting `Vertex`.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If the given vertex is invalid.
|
|||
|
"""
|
|||
|
# Accepting _mgp.Path is just for internal usage.
|
|||
|
if isinstance(starting_vertex_or_path, _mgp_mock.Path):
|
|||
|
self._path = starting_vertex_or_path
|
|||
|
|
|||
|
elif isinstance(starting_vertex_or_path, Vertex):
|
|||
|
# For consistency with the Python API, the `_vertex` attribute isn’t a “public” property.
|
|||
|
vertex = starting_vertex_or_path._vertex
|
|||
|
if not vertex.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
self._path = _mgp_mock.Path.make_with_start(vertex)
|
|||
|
|
|||
|
else:
|
|||
|
raise TypeError(f"Expected 'Vertex' or '_mgp_mock.Path', got '{type(starting_vertex_or_path)}'")
|
|||
|
|
|||
|
self._vertices = None
|
|||
|
self._edges = None
|
|||
|
|
|||
|
def __copy__(self):
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
assert len(self.vertices) >= 1
|
|||
|
|
|||
|
path = Path(self.vertices[0])
|
|||
|
for e in self.edges:
|
|||
|
path.expand(e)
|
|||
|
|
|||
|
return path
|
|||
|
|
|||
|
def __deepcopy__(self, memo):
|
|||
|
# In line with the Python API, this is the same as the shallow copy.
|
|||
|
try:
|
|||
|
return Path(memo[id(self._path)])
|
|||
|
except KeyError:
|
|||
|
pass
|
|||
|
path = self.__copy__()
|
|||
|
memo[id(self._path)] = path._path
|
|||
|
return path
|
|||
|
|
|||
|
def is_valid(self) -> bool:
|
|||
|
"""
|
|||
|
Check if the path is in a valid context, i.e. if it may be used.
|
|||
|
|
|||
|
Returns:
|
|||
|
A `bool` value that represents whether the path is in a valid context.
|
|||
|
|
|||
|
Examples:
|
|||
|
```path.is_valid()```
|
|||
|
"""
|
|||
|
return self._path.is_valid()
|
|||
|
|
|||
|
def expand(self, edge: Edge):
|
|||
|
"""
|
|||
|
Append an edge continuing from the last vertex on the path.
|
|||
|
The destination (head) of the given edge will become the last vertex in the path.
|
|||
|
|
|||
|
Args:
|
|||
|
edge: The `Edge` to be added to the path.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If using an invalid `Path` instance or if the given `Edge` is invalid.
|
|||
|
LogicErrorError: If the current last vertex in the path is not part of the given edge.
|
|||
|
|
|||
|
Examples:
|
|||
|
```path.expand(edge)```
|
|||
|
"""
|
|||
|
if not isinstance(edge, Edge):
|
|||
|
raise TypeError(f"Expected 'Edge', got '{type(edge)}'")
|
|||
|
|
|||
|
if not self.is_valid() or not edge.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
# For consistency with the Python API, the `_edge` attribute isn’t a “public” property.
|
|||
|
self._path.expand(edge._edge)
|
|||
|
|
|||
|
self._vertices = None
|
|||
|
self._edges = None
|
|||
|
|
|||
|
@property
|
|||
|
def vertices(self) -> typing.Tuple[Vertex, ...]:
|
|||
|
"""
|
|||
|
Get the path’s vertices in a fixed order.
|
|||
|
|
|||
|
Returns:
|
|||
|
A `Tuple` of the path’s vertices (`Vertex`) ordered from the start to the end of the path.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If using an invalid Path instance.
|
|||
|
|
|||
|
Examples:
|
|||
|
```path.vertices```
|
|||
|
"""
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
if self._vertices is None:
|
|||
|
num_vertices = self._path.size() + 1
|
|||
|
self._vertices = tuple(Vertex(self._path.vertex_at(i)) for i in range(num_vertices))
|
|||
|
|
|||
|
return self._vertices
|
|||
|
|
|||
|
@property
|
|||
|
def edges(self) -> typing.Tuple[Edge, ...]:
|
|||
|
"""
|
|||
|
Get the path’s edges in a fixed order.
|
|||
|
|
|||
|
Returns:
|
|||
|
A `Tuple` of the path’s edges (`Edges`) ordered from the start to the end of the path.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If using an invalid Path instance.
|
|||
|
|
|||
|
Examples:
|
|||
|
```path.vertices```
|
|||
|
"""
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
if self._edges is None:
|
|||
|
num_edges = self._path.size()
|
|||
|
self._edges = tuple(Edge(self._path.edge_at(i)) for i in range(num_edges))
|
|||
|
|
|||
|
return self._edges
|
|||
|
|
|||
|
|
|||
|
class Vertices:
|
|||
|
"""An iterable structure of the vertices in a graph."""
|
|||
|
|
|||
|
__slots__ = ("_graph", "_len")
|
|||
|
|
|||
|
def __init__(self, graph):
|
|||
|
if not isinstance(graph, _mgp_mock.Graph):
|
|||
|
raise TypeError(f"Expected '_mgp_mock.Graph', got '{type(graph)}'")
|
|||
|
|
|||
|
self._graph = graph
|
|||
|
self._len = None
|
|||
|
|
|||
|
def __deepcopy__(self, memo):
|
|||
|
# In line with the Python API, this is the same as the shallow copy.
|
|||
|
return Vertices(self._graph)
|
|||
|
|
|||
|
def is_valid(self) -> bool:
|
|||
|
"""
|
|||
|
Check if the `Vertices` object is in a valid context, i.e. if it may be used.
|
|||
|
|
|||
|
Returns:
|
|||
|
A `bool` value that represents whether the object is in a valid context.
|
|||
|
|
|||
|
Examples:
|
|||
|
```vertices.is_valid()```
|
|||
|
"""
|
|||
|
return self._graph.is_valid()
|
|||
|
|
|||
|
def __iter__(self) -> typing.Iterable[Vertex]:
|
|||
|
"""
|
|||
|
Iterate over a graph’s vertices.
|
|||
|
|
|||
|
Returns:
|
|||
|
An `Iterable` of `Vertex` objects.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If context is invalid.
|
|||
|
|
|||
|
Examples:
|
|||
|
```
|
|||
|
for vertex in graph.vertices:
|
|||
|
```
|
|||
|
```
|
|||
|
iter(graph.vertices)
|
|||
|
```
|
|||
|
"""
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
for vertex in self._graph.vertices:
|
|||
|
yield Vertex(vertex)
|
|||
|
|
|||
|
def __contains__(self, vertex: Vertex):
|
|||
|
"""
|
|||
|
Check if the given vertex is one of the graph vertices.
|
|||
|
|
|||
|
Args:
|
|||
|
vertex: The `Vertex` to be checked.
|
|||
|
|
|||
|
Returns:
|
|||
|
A Boolean value that represents whether the given vertex is one of the graph vertices.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If the `Vertices` instance or the givern vertex is not in a valid context.
|
|||
|
|
|||
|
Examples:
|
|||
|
```if vertex in graph.vertices:```
|
|||
|
"""
|
|||
|
if not self.is_valid() or not vertex.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
return self._graph.has_node(vertex.id)
|
|||
|
|
|||
|
def __len__(self):
|
|||
|
"""
|
|||
|
Get the count of the graph vertices.
|
|||
|
|
|||
|
Returns:
|
|||
|
The count of the vertices in the graph.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If the `Vertices` instance is not in a valid context.
|
|||
|
|
|||
|
Examples:
|
|||
|
```len(graph.vertices)```
|
|||
|
"""
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
if not self._len:
|
|||
|
self._len = sum(1 for _ in self)
|
|||
|
|
|||
|
return self._len
|
|||
|
|
|||
|
|
|||
|
class Record:
|
|||
|
"""Represents a record as returned by a query procedure.
|
|||
|
Records are comprised of key (field name) - value pairs."""
|
|||
|
|
|||
|
__slots__ = ("fields",)
|
|||
|
|
|||
|
def __init__(self, **kwargs):
|
|||
|
"""Initialize with {name}={value} fields in kwargs."""
|
|||
|
self.fields = kwargs
|
|||
|
|
|||
|
def __str__(self):
|
|||
|
return str(self.fields)
|
|||
|
|
|||
|
|
|||
|
class Graph:
|
|||
|
"""The graph that stands in for Memgraph’s graph."""
|
|||
|
|
|||
|
__slots__ = ("_graph",)
|
|||
|
|
|||
|
def __init__(self, graph):
|
|||
|
if not isinstance(graph, _mgp_mock.Graph):
|
|||
|
raise TypeError(f"Expected '_mgp_mock.Graph', got '{type(graph)}'")
|
|||
|
|
|||
|
self._graph = graph
|
|||
|
|
|||
|
def __deepcopy__(self, memo):
|
|||
|
# In line with the Python API, this is the same as the shallow copy.
|
|||
|
return Graph(self._graph)
|
|||
|
|
|||
|
def is_valid(self) -> bool:
|
|||
|
"""
|
|||
|
Check if the graph is in a valid context, i.e. if it may be used.
|
|||
|
|
|||
|
Returns:
|
|||
|
A `bool` value that represents whether the graph is in a valid context.
|
|||
|
|
|||
|
Examples:
|
|||
|
```graph.is_valid()```
|
|||
|
"""
|
|||
|
return self._graph.is_valid()
|
|||
|
|
|||
|
def get_vertex_by_id(self, vertex_id: VertexId) -> Vertex:
|
|||
|
"""
|
|||
|
Return the graph vertex with the given vertex_id.
|
|||
|
Access to a `Vertex` is only valid during a single execution of a
|
|||
|
procedure in a query. You should not globally store the returned
|
|||
|
vertex.
|
|||
|
|
|||
|
Args:
|
|||
|
vertex_id: A Memgraph vertex ID (`Vertex ID`)
|
|||
|
|
|||
|
Returns:
|
|||
|
The `Vertex` with the given ID.
|
|||
|
|
|||
|
Raises:
|
|||
|
IndexError: If unable to find the given vertex_id.
|
|||
|
InvalidContextError: If context is invalid.
|
|||
|
|
|||
|
Examples:
|
|||
|
```graph.get_vertex_by_id(vertex_id)```
|
|||
|
"""
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
return Vertex(self._graph.get_vertex_by_id(vertex_id))
|
|||
|
|
|||
|
@property
|
|||
|
def vertices(self) -> Vertices:
|
|||
|
"""
|
|||
|
Get all graph vertices.
|
|||
|
|
|||
|
Access to a `Vertex` is only valid during a single execution of a
|
|||
|
query procedure. You should not globally store the returned `Vertex`
|
|||
|
instances.
|
|||
|
|
|||
|
Returns:
|
|||
|
`Vertices` in the graph.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If context is invalid.
|
|||
|
|
|||
|
Examples:
|
|||
|
Iteration over all graph vertices.
|
|||
|
|
|||
|
```
|
|||
|
graph = context.graph
|
|||
|
for vertex in graph.vertices:
|
|||
|
```
|
|||
|
"""
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
return Vertices(self._graph)
|
|||
|
|
|||
|
def is_mutable(self) -> bool:
|
|||
|
"""
|
|||
|
Check if the graph is mutable, i.e. if it can be modified.
|
|||
|
|
|||
|
Returns:
|
|||
|
A `bool` value that represents whether the graph is mutable.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If the graph is not in a valid context.
|
|||
|
|
|||
|
Examples:
|
|||
|
```graph.is_mutable()```
|
|||
|
"""
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
return not self._graph.is_immutable()
|
|||
|
|
|||
|
def create_vertex(self) -> Vertex:
|
|||
|
"""
|
|||
|
Create an empty vertex.
|
|||
|
|
|||
|
Returns:
|
|||
|
The created `Vertex`.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If the graph is not in a valid context.
|
|||
|
ImmutableObjectError: If the graph is immutable.
|
|||
|
|
|||
|
Examples:
|
|||
|
Creating an empty vertex:
|
|||
|
```vertex = graph.create_vertex()```
|
|||
|
"""
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
if self._graph.is_immutable():
|
|||
|
raise ImmutableObjectError("Cannot modify immutable object.")
|
|||
|
|
|||
|
return Vertex(self._graph.create_vertex())
|
|||
|
|
|||
|
def delete_vertex(self, vertex: Vertex) -> None:
|
|||
|
"""
|
|||
|
Delete the given vertex if it’s isolated, i.e. if there are no edges connected to it.
|
|||
|
|
|||
|
Args:
|
|||
|
vertex: The `Vertex` to be deleted.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If the graph is not in a valid context.
|
|||
|
ImmutableObjectError: If the graph is immutable.
|
|||
|
LogicErrorError: If the vertex is not isolated.
|
|||
|
|
|||
|
Examples:
|
|||
|
```graph.delete_vertex(vertex)```
|
|||
|
"""
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
if self._graph.is_immutable():
|
|||
|
raise ImmutableObjectError("Cannot modify immutable object.")
|
|||
|
|
|||
|
if not self._graph.vertex_is_isolate(vertex.id):
|
|||
|
raise LogicErrorError("Logic error.")
|
|||
|
|
|||
|
self._graph.delete_vertex(vertex.id)
|
|||
|
|
|||
|
def detach_delete_vertex(self, vertex: Vertex) -> None:
|
|||
|
"""
|
|||
|
Delete the given vertex together with all connected edges.
|
|||
|
|
|||
|
Args:
|
|||
|
vertex: The `Vertex` to be deleted.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If the graph is not in a valid context.
|
|||
|
ImmutableObjectError: If the graph is immutable.
|
|||
|
|
|||
|
Examples:
|
|||
|
```graph.detach_delete_vertex(vertex)```
|
|||
|
"""
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
if self._graph.is_immutable():
|
|||
|
raise ImmutableObjectError("Cannot modify immutable object.")
|
|||
|
|
|||
|
self._graph.delete_vertex(vertex.id)
|
|||
|
|
|||
|
def create_edge(self, from_vertex: Vertex, to_vertex: Vertex, edge_type: EdgeType) -> Edge:
|
|||
|
"""
|
|||
|
Create an empty edge.
|
|||
|
|
|||
|
Args:
|
|||
|
from_vertex: The source (tail) `Vertex`.
|
|||
|
to_vertex: The destination (head) `Vertex`.
|
|||
|
edge_type: `EdgeType` specifying the new edge’s type.
|
|||
|
|
|||
|
Returns:
|
|||
|
The created `Edge`.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If the graph is not in a valid context.
|
|||
|
ImmutableObjectError: If the graph is immutable.
|
|||
|
DeletedObjectError: If `from_vertex` or `to_vertex` have been deleted.
|
|||
|
|
|||
|
Examples:
|
|||
|
```edge = graph.create_edge(from_vertex, vertex, edge_type)```
|
|||
|
"""
|
|||
|
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
if self._graph.is_immutable():
|
|||
|
raise ImmutableObjectError("Cannot modify immutable object.")
|
|||
|
|
|||
|
new_edge = self._graph.create_edge(from_vertex._vertex, to_vertex._vertex, edge_type.name)
|
|||
|
return Edge(new_edge)
|
|||
|
|
|||
|
def delete_edge(self, edge: Edge) -> None:
|
|||
|
"""
|
|||
|
Delete the given edge.
|
|||
|
|
|||
|
Args:
|
|||
|
edge: The `Edge` to be deleted.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If the graph is not in a valid context.
|
|||
|
ImmutableObjectError: If the graph is immutable.
|
|||
|
|
|||
|
Examples:
|
|||
|
```graph.delete_edge(edge)```
|
|||
|
"""
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
if self._graph.is_immutable():
|
|||
|
raise ImmutableObjectError("Cannot modify immutable object.")
|
|||
|
|
|||
|
self._graph.delete_edge(edge.from_vertex.id, edge.to_vertex.id, edge.id)
|
|||
|
|
|||
|
|
|||
|
class AbortError(Exception):
|
|||
|
"""Signals that the procedure was asked to abort its execution."""
|
|||
|
|
|||
|
pass
|
|||
|
|
|||
|
|
|||
|
class ProcCtx:
|
|||
|
"""The context of the procedure being executed.
|
|||
|
|
|||
|
Access to a `ProcCtx` is only valid during a single execution of a query procedure.
|
|||
|
You should not globally store a `ProcCtx` instance.
|
|||
|
"""
|
|||
|
|
|||
|
__slots__ = ("_graph",)
|
|||
|
|
|||
|
def __init__(self, graph):
|
|||
|
if not isinstance(graph, (_mgp_mock.Graph, _mgp_mock.nx.MultiDiGraph)):
|
|||
|
raise TypeError(f"Expected '_mgp_mock.Graph' or 'networkx.MultiDiGraph', got '{type(graph)}'")
|
|||
|
|
|||
|
self._graph = Graph(graph) if isinstance(graph, _mgp_mock.Graph) else Graph(_mgp_mock.Graph(graph))
|
|||
|
|
|||
|
def is_valid(self) -> bool:
|
|||
|
"""
|
|||
|
Check if the context is valid, i.e. if the contained structures may be used.
|
|||
|
|
|||
|
Returns:
|
|||
|
A `bool` value that represents whether the context is valid.
|
|||
|
|
|||
|
Examples:
|
|||
|
```context.is_valid()```
|
|||
|
"""
|
|||
|
return self._graph.is_valid()
|
|||
|
|
|||
|
@property
|
|||
|
def graph(self) -> Graph:
|
|||
|
"""
|
|||
|
Access the graph.
|
|||
|
|
|||
|
Returns:
|
|||
|
A `Graph` object representing the graph.
|
|||
|
|
|||
|
Raises:
|
|||
|
InvalidContextError: If the procedure context is not valid.
|
|||
|
|
|||
|
Examples:
|
|||
|
```context.graph```
|
|||
|
"""
|
|||
|
if not self.is_valid():
|
|||
|
raise InvalidContextError()
|
|||
|
|
|||
|
return self._graph
|
|||
|
|
|||
|
|
|||
|
# Additional typing support
|
|||
|
|
|||
|
Number = typing.Union[int, float]
|
|||
|
|
|||
|
Map = typing.Union[dict, Edge, Vertex]
|
|||
|
|
|||
|
Date = datetime.date
|
|||
|
|
|||
|
LocalTime = datetime.time
|
|||
|
|
|||
|
LocalDateTime = datetime.datetime
|
|||
|
|
|||
|
Duration = datetime.timedelta
|
|||
|
|
|||
|
Any = typing.Union[bool, str, Number, Map, list, Date, LocalTime, LocalDateTime, Duration]
|
|||
|
|
|||
|
List = typing.List
|
|||
|
|
|||
|
Nullable = typing.Optional
|
|||
|
|
|||
|
|
|||
|
# Procedure registration
|
|||
|
|
|||
|
|
|||
|
def raise_if_does_not_meet_requirements(func: typing.Callable[..., Record]):
|
|||
|
if not callable(func):
|
|||
|
raise TypeError(f"Expected a callable object, got an instance of '{type(func)}'")
|
|||
|
if inspect.iscoroutinefunction(func):
|
|||
|
raise TypeError("Callable must not be 'async def' function")
|
|||
|
if sys.version_info >= (3, 6):
|
|||
|
if inspect.isasyncgenfunction(func):
|
|||
|
raise TypeError("Callable must not be 'async def' function")
|
|||
|
if inspect.isgeneratorfunction(func):
|
|||
|
raise NotImplementedError("Generator functions are not supported")
|
|||
|
|
|||
|
|
|||
|
def _register_proc(func: typing.Callable[..., Record], is_write: bool):
|
|||
|
raise_if_does_not_meet_requirements(func)
|
|||
|
|
|||
|
sig = inspect.signature(func)
|
|||
|
|
|||
|
params = tuple(sig.parameters.values())
|
|||
|
if params and params[0].annotation is ProcCtx:
|
|||
|
|
|||
|
@wraps(func)
|
|||
|
def wrapper(ctx, *args):
|
|||
|
result_record = None
|
|||
|
|
|||
|
if is_write:
|
|||
|
ctx_copy = ProcCtx(deepcopy(ctx._graph._graph.nx))
|
|||
|
|
|||
|
result_record = func(ctx_copy, *args)
|
|||
|
|
|||
|
ctx._graph._graph = deepcopy(ctx_copy._graph._graph)
|
|||
|
|
|||
|
# Invalidate context after execution
|
|||
|
ctx_copy._graph._graph.invalidate()
|
|||
|
else:
|
|||
|
ctx._graph._graph.make_immutable()
|
|||
|
|
|||
|
result_record = func(ctx, *args)
|
|||
|
|
|||
|
# Invalidate context after execution
|
|||
|
ctx._graph._graph.invalidate()
|
|||
|
|
|||
|
return result_record
|
|||
|
|
|||
|
else:
|
|||
|
|
|||
|
@wraps(func)
|
|||
|
def wrapper(*args):
|
|||
|
return func(*args)
|
|||
|
|
|||
|
if sig.return_annotation is not sig.empty:
|
|||
|
record = sig.return_annotation
|
|||
|
|
|||
|
if not isinstance(record, Record):
|
|||
|
raise TypeError(f"Expected '{func.__name__}' to return 'mgp.Record', got '{type(record)}'")
|
|||
|
|
|||
|
return wrapper
|
|||
|
|
|||
|
|
|||
|
def read_proc(func: typing.Callable[..., Record]):
|
|||
|
"""
|
|||
|
Register a function as a Memgraph read-only procedure.
|
|||
|
|
|||
|
The `func` needs to be a callable and optionally take `ProcCtx` as its first argument.
|
|||
|
Other parameters of `func` will be bound to the passed arguments.
|
|||
|
The full signature of `func` needs to be annotated with types. The return type must
|
|||
|
be `Record(field_name=type, ...)`, and the procedure must produce either a complete
|
|||
|
Record or None. Multiple records can be produced by returning an iterable of them.
|
|||
|
Registering generator functions is currently not supported.
|
|||
|
|
|||
|
Example:
|
|||
|
```
|
|||
|
import mgp_mock
|
|||
|
|
|||
|
@mgp_mock.read_proc
|
|||
|
def procedure(context: mgp_mock.ProcCtx,
|
|||
|
required_arg: mgp_mock.Nullable[mgp_mock.Any],
|
|||
|
optional_arg: mgp_mock.Nullable[mgp_mock.Any] = None
|
|||
|
) -> mgp_mock.Record(result=str, args=list):
|
|||
|
args = [required_arg, optional_arg]
|
|||
|
# Multiple rows can be produced by returning an iterable of mgp_mock.Record:
|
|||
|
return mgp_mock.Record(args=args, result="Hello World!")
|
|||
|
```
|
|||
|
|
|||
|
The above example procedure returns two fields: `args` and `result`.
|
|||
|
* `args` is a copy of the arguments passed to the procedure.
|
|||
|
* `result` is "Hello World!".
|
|||
|
|
|||
|
Any errors can be reported by raising an Exception.
|
|||
|
"""
|
|||
|
return _register_proc(func, False)
|
|||
|
|
|||
|
|
|||
|
def write_proc(func: typing.Callable[..., Record]):
|
|||
|
"""
|
|||
|
Register a function as a Memgraph write procedure.
|
|||
|
|
|||
|
The `func` needs to be a callable and optionally take `ProcCtx` as its first argument.
|
|||
|
Other parameters of `func` will be bound to the passed arguments.
|
|||
|
The full signature of `func` needs to be annotated with types. The return type must
|
|||
|
be `Record(field_name=type, ...)`, and the procedure must produce either a complete
|
|||
|
Record or None. Multiple records can be produced by returning an iterable of them.
|
|||
|
Registering generator functions is currently not supported.
|
|||
|
|
|||
|
Example:
|
|||
|
```
|
|||
|
import mgp_mock
|
|||
|
|
|||
|
@mgp_mock.write_proc
|
|||
|
def procedure(context: mgp_mock.ProcCtx,
|
|||
|
required_arg: str,
|
|||
|
optional_arg: mgp_mock.Nullable[str] = None
|
|||
|
) -> mgp_mock.Record(result=mgp_mock.Vertex):
|
|||
|
vertex = context.graph.create_vertex()
|
|||
|
vertex_properties = vertex.properties
|
|||
|
vertex_properties["required_arg"] = required_arg
|
|||
|
if optional_arg is not None:
|
|||
|
vertex_properties["optional_arg"] = optional_arg
|
|||
|
|
|||
|
return mgp.Record(result=vertex)
|
|||
|
```
|
|||
|
|
|||
|
The above example procedure returns a newly created vertex that has
|
|||
|
up to 2 properties:
|
|||
|
* `required_arg` is always present and its value is the first
|
|||
|
argument of the procedure.
|
|||
|
* `optional_arg` is present if the second argument of the procedure
|
|||
|
is not `null`.
|
|||
|
|
|||
|
Any errors can be reported by raising an Exception.
|
|||
|
"""
|
|||
|
return _register_proc(func, True)
|
|||
|
|
|||
|
|
|||
|
class FuncCtx:
|
|||
|
"""The context of the function being executed.
|
|||
|
|
|||
|
Access to a `FuncCtx` is only valid during a single execution of a transformation.
|
|||
|
You should not globally store a `FuncCtx` instance.
|
|||
|
The graph object within `FuncCtx` is not mutable.
|
|||
|
"""
|
|||
|
|
|||
|
__slots__ = ("_graph",)
|
|||
|
|
|||
|
def __init__(self, graph):
|
|||
|
if not isinstance(graph, (_mgp_mock.Graph, _mgp_mock.nx.MultiDiGraph)):
|
|||
|
raise TypeError(f"Expected '_mgp_mock.Graph' or 'networkx.MultiDiGraph', got '{type(graph)}'")
|
|||
|
|
|||
|
self._graph = Graph(graph) if isinstance(graph, _mgp_mock.Graph) else Graph(_mgp_mock.Graph(graph))
|
|||
|
|
|||
|
def is_valid(self) -> bool:
|
|||
|
"""
|
|||
|
Check if the context is valid, i.e. if the contained structures may be used.
|
|||
|
|
|||
|
Returns:
|
|||
|
A `bool` value that represents whether the context is valid.
|
|||
|
|
|||
|
Examples:
|
|||
|
```context.is_valid()```
|
|||
|
"""
|
|||
|
return self._graph.is_valid()
|
|||
|
|
|||
|
|
|||
|
def function(func: typing.Callable):
|
|||
|
"""
|
|||
|
Register a function as a Memgraph function.
|
|||
|
|
|||
|
The `func` needs to be a callable and optionally take `ProcCtx` as its first argument.
|
|||
|
Other parameters of `func` will be bound to the passed arguments.
|
|||
|
Only the function arguments need to be annotated with types. The return type doesn’t
|
|||
|
need to be specified, but it has to be within `mgp_mock.Any`.
|
|||
|
Registering generator functions is currently not supported.
|
|||
|
|
|||
|
Example:
|
|||
|
```
|
|||
|
import mgp_mock
|
|||
|
|
|||
|
@mgp_mock.function
|
|||
|
def procedure(context: mgp_mock.FuncCtx,
|
|||
|
required_arg: str,
|
|||
|
optional_arg: mgp_mock.Nullable[str] = None
|
|||
|
):
|
|||
|
return_args = [required_arg]
|
|||
|
if optional_arg is not None:
|
|||
|
return_args.append(optional_arg)
|
|||
|
# Return any result whose type is within mgp_mock.Any
|
|||
|
return return_args
|
|||
|
```
|
|||
|
|
|||
|
The above example function returns a list of the passed parameters:
|
|||
|
* `required_arg` is always present and its value is the first
|
|||
|
argument of the procedure.
|
|||
|
* `optional_arg` is present if the second argument of the procedure
|
|||
|
is not `null`.
|
|||
|
|
|||
|
Any errors can be reported by raising an Exception.
|
|||
|
"""
|
|||
|
raise_if_does_not_meet_requirements(func)
|
|||
|
|
|||
|
sig = inspect.signature(func)
|
|||
|
|
|||
|
params = tuple(sig.parameters.values())
|
|||
|
if params and params[0].annotation is FuncCtx:
|
|||
|
|
|||
|
@wraps(func)
|
|||
|
def wrapper(ctx, *args):
|
|||
|
ctx._graph._graph.make_immutable()
|
|||
|
|
|||
|
result = func(ctx, *args)
|
|||
|
|
|||
|
# Invalidate context after execution
|
|||
|
ctx._graph._graph.invalidate()
|
|||
|
|
|||
|
return result
|
|||
|
|
|||
|
else:
|
|||
|
|
|||
|
@wraps(func)
|
|||
|
def wrapper(*args):
|
|||
|
return func(*args)
|
|||
|
|
|||
|
return wrapper
|
|||
|
|
|||
|
|
|||
|
def _wrap_exceptions():
|
|||
|
def wrap_function(func):
|
|||
|
@wraps(func)
|
|||
|
def wrapped_func(*args, **kwargs):
|
|||
|
try:
|
|||
|
return func(*args, **kwargs)
|
|||
|
except _mgp_mock.LogicErrorError as e:
|
|||
|
raise LogicErrorError(e)
|
|||
|
except _mgp_mock.ImmutableObjectError as e:
|
|||
|
raise ImmutableObjectError(e)
|
|||
|
|
|||
|
return wrapped_func
|
|||
|
|
|||
|
def wrap_prop_func(func):
|
|||
|
return None if func is None else wrap_function(func)
|
|||
|
|
|||
|
def wrap_member_functions(cls: type):
|
|||
|
for name, obj in inspect.getmembers(cls):
|
|||
|
if inspect.isfunction(obj):
|
|||
|
setattr(cls, name, wrap_function(obj))
|
|||
|
elif isinstance(obj, property):
|
|||
|
setattr(
|
|||
|
cls,
|
|||
|
name,
|
|||
|
property(
|
|||
|
wrap_prop_func(obj.fget),
|
|||
|
wrap_prop_func(obj.fset),
|
|||
|
wrap_prop_func(obj.fdel),
|
|||
|
obj.__doc__,
|
|||
|
),
|
|||
|
)
|
|||
|
|
|||
|
def defined_in_this_module(obj: object):
|
|||
|
return getattr(obj, "__module__", "") == __name__
|
|||
|
|
|||
|
module = sys.modules[__name__]
|
|||
|
for name, obj in inspect.getmembers(module):
|
|||
|
if not defined_in_this_module(obj):
|
|||
|
continue
|
|||
|
if inspect.isclass(obj):
|
|||
|
wrap_member_functions(obj)
|
|||
|
elif inspect.isfunction(obj) and not name.startswith("_"):
|
|||
|
setattr(module, name, wrap_function(obj))
|
|||
|
|
|||
|
|
|||
|
_wrap_exceptions()
|