2021-10-03 18:07:04 +08:00
|
|
|
|
# Copyright 2021 Memgraph Ltd.
|
|
|
|
|
#
|
|
|
|
|
# Use of this software is governed by the Business Source License
|
2021-10-13 16:06:07 +08:00
|
|
|
|
# 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.
|
2021-10-03 18:07:04 +08:00
|
|
|
|
#
|
|
|
|
|
# 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.
|
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
2020-02-04 16:29:38 +08:00
|
|
|
|
This module provides the API for usage in custom openCypher procedures.
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
# C API using `mgp_memory` is not exposed in Python, instead the usage of such
|
|
|
|
|
# API is hidden behind Python API. Any function requiring an instance of
|
|
|
|
|
# `mgp_memory` should go through a `ProcCtx` instance.
|
|
|
|
|
#
|
|
|
|
|
# `mgp_value` does not exist as such in Python, instead all `mgp_value`
|
|
|
|
|
# instances are marshalled to an appropriate Python object. This implies that
|
|
|
|
|
# `mgp_list` and `mgp_map` are mapped to `list` and `dict` respectively.
|
|
|
|
|
#
|
|
|
|
|
# Only the public API is stubbed out here. Any private details are left for the
|
|
|
|
|
# actual implementation. Functions have type annotations as supported by Python
|
|
|
|
|
# 3.5, but variable type annotations are only available with Python 3.6+
|
|
|
|
|
|
|
|
|
|
from collections import namedtuple
|
2021-09-22 14:49:29 +08:00
|
|
|
|
from functools import wraps
|
Register Python procedures
Summary:
With this diff you should now be able to register `example.py` and read
procedures found there. The procedures will be listed through `CALL
mg.procedures() YIELD *` query, but invoking them will raise
`NotYetImplemented`.
If you wish to test this, you will need to run the Memgraph executable
with PYTHONPATH set to the `include` directory where `mgp.py` is found.
Additionally, you need to pass `--query-modules-directory` flag to
Memgraph, such that it points to where it will find the `example.py`.
For example, when running from the root directory of Memgraph repo, the
shell invocation below should do the trick (assuming `./build/memgraph`
is where is the executable). Make sure that `./query_modules/` does not
have `example.so` built, as that may interfere with loading
`example.py`.
PYTHONPATH=$PWD/include ./build/memgraph --query-modules-directory=./query_modules/
Reviewers: mferencevic, ipaljak, llugovic
Reviewed By: mferencevic, ipaljak, llugovic
Subscribers: pullbot
Differential Revision: https://phabricator.memgraph.io/D2678
2020-02-19 17:39:18 +08:00
|
|
|
|
import inspect
|
|
|
|
|
import sys
|
2020-02-04 16:29:38 +08:00
|
|
|
|
import typing
|
2021-09-20 16:59:30 +08:00
|
|
|
|
import datetime
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
|
2020-02-12 17:27:59 +08:00
|
|
|
|
import _mgp
|
|
|
|
|
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2020-03-03 16:51:15 +08:00
|
|
|
|
class InvalidContextError(Exception):
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Signals using a graph element instance outside of the registered procedure.
|
|
|
|
|
"""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UnknownError(_mgp.UnknownError):
|
|
|
|
|
"""
|
|
|
|
|
Signals unspecified failure.
|
|
|
|
|
"""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UnableToAllocateError(_mgp.UnableToAllocateError):
|
|
|
|
|
"""
|
|
|
|
|
Signals failed memory allocation.
|
|
|
|
|
"""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InsufficientBufferError(_mgp.InsufficientBufferError):
|
|
|
|
|
"""
|
|
|
|
|
Signals that some buffer is not big enough.
|
|
|
|
|
"""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OutOfRangeError(_mgp.OutOfRangeError):
|
|
|
|
|
"""
|
|
|
|
|
Signals that an index-like parameter has a value that is outside its
|
|
|
|
|
possible values.
|
|
|
|
|
"""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LogicErrorError(_mgp.LogicErrorError):
|
|
|
|
|
"""
|
|
|
|
|
Signals faulty logic within the program such as violating logical
|
|
|
|
|
preconditions or class invariants and may be preventable.
|
|
|
|
|
"""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DeletedObjectError(_mgp.DeletedObjectError):
|
|
|
|
|
"""
|
|
|
|
|
Signals accessing an already deleted object.
|
|
|
|
|
"""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InvalidArgumentError(_mgp.InvalidArgumentError):
|
|
|
|
|
"""
|
|
|
|
|
Signals that some of the arguments have invalid values.
|
|
|
|
|
"""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class KeyAlreadyExistsError(_mgp.KeyAlreadyExistsError):
|
|
|
|
|
"""
|
|
|
|
|
Signals that a key already exists in a container-like object.
|
|
|
|
|
"""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ImmutableObjectError(_mgp.ImmutableObjectError):
|
|
|
|
|
"""
|
|
|
|
|
Signals modification of an immutable object.
|
|
|
|
|
"""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ValueConversionError(_mgp.ValueConversionError):
|
|
|
|
|
"""
|
|
|
|
|
Signals that the conversion failed between python and cypher values.
|
|
|
|
|
"""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SerializationError(_mgp.SerializationError):
|
|
|
|
|
"""
|
|
|
|
|
Signals serialization error caused by concurrent modifications from
|
|
|
|
|
different transactions.
|
|
|
|
|
"""
|
2020-03-03 16:51:15 +08:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2020-02-04 16:29:38 +08:00
|
|
|
|
class Label:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Label of a Vertex."""
|
2020-02-26 20:13:36 +08:00
|
|
|
|
__slots__ = ('_name',)
|
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
def __init__(self, name: str):
|
2020-02-27 19:24:26 +08:00
|
|
|
|
self._name = name
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def name(self) -> str:
|
2020-02-27 19:24:26 +08:00
|
|
|
|
return self._name
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2020-02-26 20:13:36 +08:00
|
|
|
|
def __eq__(self, other) -> bool:
|
|
|
|
|
if isinstance(other, Label):
|
|
|
|
|
return self._name == other.name
|
|
|
|
|
if isinstance(other, str):
|
|
|
|
|
return self._name == other
|
|
|
|
|
return NotImplemented
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2020-02-27 19:24:26 +08:00
|
|
|
|
|
2020-02-04 16:29:38 +08:00
|
|
|
|
# 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:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
A collection of properties either on a Vertex or an Edge.
|
|
|
|
|
"""
|
2020-03-10 21:12:57 +08:00
|
|
|
|
__slots__ = ('_vertex_or_edge', '_len',)
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2020-03-10 21:12:57 +08:00
|
|
|
|
def __init__(self, vertex_or_edge):
|
|
|
|
|
if not isinstance(vertex_or_edge, (_mgp.Vertex, _mgp.Edge)):
|
|
|
|
|
raise TypeError("Expected '_mgp.Vertex' or '_mgp.Edge', \
|
|
|
|
|
got {}".format(type(vertex_or_edge)))
|
|
|
|
|
self._len = None
|
|
|
|
|
self._vertex_or_edge = vertex_or_edge
|
2020-02-25 17:24:16 +08:00
|
|
|
|
|
2020-10-02 02:51:55 +08:00
|
|
|
|
def __deepcopy__(self, memo):
|
|
|
|
|
# This is the same as the shallow copy, as the underlying C API should
|
|
|
|
|
# not support deepcopy. Besides, it doesn't make much sense to actually
|
|
|
|
|
# copy _mgp.Edge and _mgp.Vertex types as they are actually references
|
|
|
|
|
# to graph elements and not proper values.
|
|
|
|
|
return Properties(self._vertex_or_edge)
|
|
|
|
|
|
2020-02-04 16:29:38 +08:00
|
|
|
|
def get(self, property_name: str, default=None) -> object:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Get the value of a property with the given name or return default.
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2020-03-03 16:51:15 +08:00
|
|
|
|
Raise InvalidContextError.
|
2021-09-22 14:49:29 +08:00
|
|
|
|
Raise UnableToAllocateError if unable to allocate a mgp.Value.
|
|
|
|
|
Raise DeletedObjectError if the object has been deleted.
|
|
|
|
|
"""
|
2020-03-10 21:12:57 +08:00
|
|
|
|
if not self._vertex_or_edge.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
try:
|
|
|
|
|
return self[property_name]
|
|
|
|
|
except KeyError:
|
|
|
|
|
return default
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
def set(self, property_name: str, value: object) -> None:
|
|
|
|
|
"""
|
|
|
|
|
Set the value of the property. When the value is `None`, then the
|
|
|
|
|
property is removed.
|
|
|
|
|
|
|
|
|
|
Raise UnableToAllocateError if unable to allocate memory for storing
|
|
|
|
|
the property.
|
|
|
|
|
Raise ImmutableObjectError if the object is immutable.
|
|
|
|
|
Raise DeletedObjectError if the ojbect has been deleted.
|
|
|
|
|
Raise SerializationError if the object has been modified by another
|
|
|
|
|
transaction.
|
|
|
|
|
Raise ValueConversionError if `value` is vertex, edge or path.
|
|
|
|
|
"""
|
|
|
|
|
self[property_name] = value
|
|
|
|
|
|
2020-02-04 16:29:38 +08:00
|
|
|
|
def items(self) -> typing.Iterable[Property]:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Iterate over the properties.
|
|
|
|
|
|
2021-09-30 21:11:51 +08:00
|
|
|
|
Doesn’t return a dynamic view of the properties, but copies the
|
|
|
|
|
current properties.
|
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
Raise InvalidContextError.
|
|
|
|
|
Raise UnableToAllocateError if unable to allocate an iterator.
|
|
|
|
|
Raise DeletedObjectError if the object has been deleted.
|
|
|
|
|
"""
|
2020-03-10 21:12:57 +08:00
|
|
|
|
if not self._vertex_or_edge.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
properties_it = self._vertex_or_edge.iter_properties()
|
|
|
|
|
prop = properties_it.get()
|
|
|
|
|
while prop is not None:
|
|
|
|
|
yield Property(*prop)
|
|
|
|
|
if not self._vertex_or_edge.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
prop = properties_it.next()
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
def keys(self) -> typing.Iterable[str]:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Iterate over property names.
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2021-09-30 21:11:51 +08:00
|
|
|
|
Doesn’t return a dynamic view of the property names, but copies the
|
|
|
|
|
name of the current properties.
|
|
|
|
|
|
2020-03-03 16:51:15 +08:00
|
|
|
|
Raise InvalidContextError.
|
2021-09-22 14:49:29 +08:00
|
|
|
|
Raise UnableToAllocateError if unable to allocate an iterator.
|
|
|
|
|
Raise DeletedObjectError if the object has been deleted.
|
|
|
|
|
"""
|
2020-03-10 21:12:57 +08:00
|
|
|
|
if not self._vertex_or_edge.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
for item in self.items():
|
|
|
|
|
yield item.name
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
def values(self) -> typing.Iterable[object]:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Iterate over property values.
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2021-09-30 21:11:51 +08:00
|
|
|
|
Doesn’t return a dynamic view of the property values, but copies the
|
|
|
|
|
value of the current properties.
|
|
|
|
|
|
2020-03-03 16:51:15 +08:00
|
|
|
|
Raise InvalidContextError.
|
2021-09-22 14:49:29 +08:00
|
|
|
|
Raise UnableToAllocateError if unable to allocate an iterator.
|
|
|
|
|
Raise DeletedObjectError if the object has been deleted.
|
|
|
|
|
"""
|
2020-03-10 21:12:57 +08:00
|
|
|
|
if not self._vertex_or_edge.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
for item in self.items():
|
|
|
|
|
yield item.value
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
def __len__(self) -> int:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Get the number of properties.
|
|
|
|
|
|
|
|
|
|
Raise InvalidContextError.
|
|
|
|
|
Raise UnableToAllocateError if unable to allocate an iterator.
|
|
|
|
|
Raise DeletedObjectError if the object has been deleted.
|
|
|
|
|
"""
|
2020-03-10 21:12:57 +08:00
|
|
|
|
if not self._vertex_or_edge.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
if self._len is None:
|
|
|
|
|
self._len = sum(1 for item in self.items())
|
|
|
|
|
return self._len
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
def __iter__(self) -> typing.Iterable[str]:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Iterate over property names.
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2020-03-03 16:51:15 +08:00
|
|
|
|
Raise InvalidContextError.
|
2021-09-22 14:49:29 +08:00
|
|
|
|
Raise UnableToAllocateError if unable to allocate an iterator.
|
|
|
|
|
Raise DeletedObjectError if the object has been deleted.
|
|
|
|
|
"""
|
2020-03-10 21:12:57 +08:00
|
|
|
|
if not self._vertex_or_edge.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
for item in self.items():
|
|
|
|
|
yield item.name
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
def __getitem__(self, property_name: str) -> object:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Get the value of a property with the given name or raise KeyError.
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
Raise InvalidContextError.
|
|
|
|
|
Raise UnableToAllocateError if unable to allocate a mgp.Value.
|
|
|
|
|
Raise DeletedObjectError if the object has been deleted.
|
|
|
|
|
"""
|
2020-03-10 21:12:57 +08:00
|
|
|
|
if not self._vertex_or_edge.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
prop = self._vertex_or_edge.get_property(property_name)
|
|
|
|
|
if prop is None:
|
|
|
|
|
raise KeyError()
|
|
|
|
|
return prop
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
def __setitem__(self, property_name: str, value: object) -> None:
|
|
|
|
|
"""
|
|
|
|
|
Set the value of the property. When the value is `None`, then the
|
|
|
|
|
property is removed.
|
|
|
|
|
|
|
|
|
|
Raise UnableToAllocateError if unable to allocate memory for storing
|
|
|
|
|
the property.
|
|
|
|
|
Raise ImmutableObjectError if the object is immutable.
|
|
|
|
|
Raise DeletedObjectError if the ojbect has been deleted.
|
|
|
|
|
Raise SerializationError if the object has been modified by another
|
|
|
|
|
transaction.
|
|
|
|
|
Raise ValueConversionError if `value` is vertex, edge or path.
|
|
|
|
|
"""
|
|
|
|
|
if not self._vertex_or_edge.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
|
|
|
|
|
self._vertex_or_edge.set_property(property_name, value)
|
|
|
|
|
|
2020-02-04 16:29:38 +08:00
|
|
|
|
def __contains__(self, property_name: str) -> bool:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Check if there is a property with the given name.
|
|
|
|
|
|
|
|
|
|
Raise InvalidContextError.
|
|
|
|
|
Raise UnableToAllocateError if unable to allocate a mgp.Value.
|
|
|
|
|
Raise DeletedObjectError if the object has been deleted.
|
|
|
|
|
"""
|
2020-03-10 21:12:57 +08:00
|
|
|
|
if not self._vertex_or_edge.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
try:
|
|
|
|
|
_ = self[property_name]
|
|
|
|
|
return True
|
|
|
|
|
except KeyError:
|
|
|
|
|
return False
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class EdgeType:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Type of an Edge."""
|
2020-02-25 17:24:16 +08:00
|
|
|
|
__slots__ = ('_name',)
|
|
|
|
|
|
|
|
|
|
def __init__(self, name):
|
|
|
|
|
self._name = name
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def name(self) -> str:
|
2020-02-25 17:24:16 +08:00
|
|
|
|
return self._name
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2020-03-17 20:44:23 +08:00
|
|
|
|
def __eq__(self, other) -> bool:
|
|
|
|
|
if isinstance(other, EdgeType):
|
|
|
|
|
return self.name == other.name
|
|
|
|
|
if isinstance(other, str):
|
|
|
|
|
return self.name == other
|
|
|
|
|
return NotImplemented
|
|
|
|
|
|
2020-03-18 21:07:56 +08:00
|
|
|
|
|
|
|
|
|
if sys.version_info >= (3, 5, 2):
|
|
|
|
|
EdgeId = typing.NewType('EdgeId', int)
|
|
|
|
|
else:
|
|
|
|
|
EdgeId = int
|
2020-03-17 20:44:23 +08:00
|
|
|
|
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
class Edge:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Edge in the graph database.
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
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
|
2020-03-03 16:51:15 +08:00
|
|
|
|
invalid Edge instance will raise InvalidContextError.
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
2020-02-25 17:24:16 +08:00
|
|
|
|
__slots__ = ('_edge',)
|
|
|
|
|
|
|
|
|
|
def __init__(self, edge):
|
|
|
|
|
if not isinstance(edge, _mgp.Edge):
|
2021-07-05 17:42:40 +08:00
|
|
|
|
raise TypeError(
|
|
|
|
|
"Expected '_mgp.Edge', got '{}'".format(type(edge)))
|
2020-02-25 17:24:16 +08:00
|
|
|
|
self._edge = edge
|
|
|
|
|
|
2020-02-28 16:44:39 +08:00
|
|
|
|
def __deepcopy__(self, memo):
|
|
|
|
|
# This is the same as the shallow copy, because we want to share the
|
|
|
|
|
# underlying C struct. Besides, it doesn't make much sense to actually
|
|
|
|
|
# copy _mgp.Edge as that is actually a reference to a graph element
|
|
|
|
|
# and not a proper value.
|
|
|
|
|
return Edge(self._edge)
|
|
|
|
|
|
2020-02-25 17:24:16 +08:00
|
|
|
|
def is_valid(self) -> bool:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Return True if `self` is in valid context and may be used."""
|
2020-02-25 17:24:16 +08:00
|
|
|
|
return self._edge.is_valid()
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
def underlying_graph_is_mutable(self) -> bool:
|
|
|
|
|
"""Return True if the edge can be modified."""
|
|
|
|
|
if not self.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
return self._edge.underlying_graph_is_mutable()
|
|
|
|
|
|
2020-03-18 21:07:56 +08:00
|
|
|
|
@property
|
|
|
|
|
def id(self) -> EdgeId:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Get the ID of the edge.
|
|
|
|
|
|
|
|
|
|
Raise InvalidContextError.
|
|
|
|
|
"""
|
2020-03-18 21:07:56 +08:00
|
|
|
|
if not self.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
return self._edge.get_id()
|
|
|
|
|
|
2020-02-04 16:29:38 +08:00
|
|
|
|
@property
|
|
|
|
|
def type(self) -> EdgeType:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Get the type of the edge.
|
|
|
|
|
|
|
|
|
|
Raise InvalidContextError.
|
|
|
|
|
"""
|
2020-02-25 17:24:16 +08:00
|
|
|
|
if not self.is_valid():
|
2020-03-03 16:51:15 +08:00
|
|
|
|
raise InvalidContextError()
|
2020-02-25 17:24:16 +08:00
|
|
|
|
return EdgeType(self._edge.get_type_name())
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
@property
|
2021-10-03 17:09:57 +08:00
|
|
|
|
def from_vertex(self) -> 'Vertex':
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Get the source vertex.
|
|
|
|
|
|
|
|
|
|
Raise InvalidContextError.
|
|
|
|
|
"""
|
2020-02-25 17:24:16 +08:00
|
|
|
|
if not self.is_valid():
|
2020-03-03 16:51:15 +08:00
|
|
|
|
raise InvalidContextError()
|
2020-02-25 17:24:16 +08:00
|
|
|
|
return Vertex(self._edge.from_vertex())
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
@property
|
2021-10-03 17:09:57 +08:00
|
|
|
|
def to_vertex(self) -> 'Vertex':
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Get the destination vertex.
|
|
|
|
|
|
|
|
|
|
Raise InvalidContextError.
|
|
|
|
|
"""
|
2020-02-25 17:24:16 +08:00
|
|
|
|
if not self.is_valid():
|
2020-03-03 16:51:15 +08:00
|
|
|
|
raise InvalidContextError()
|
2020-02-25 17:24:16 +08:00
|
|
|
|
return Vertex(self._edge.to_vertex())
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def properties(self) -> Properties:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Get the properties of the edge.
|
|
|
|
|
|
|
|
|
|
Raise InvalidContextError.
|
|
|
|
|
"""
|
2020-02-25 17:24:16 +08:00
|
|
|
|
if not self.is_valid():
|
2020-03-03 16:51:15 +08:00
|
|
|
|
raise InvalidContextError()
|
2020-02-25 17:24:16 +08:00
|
|
|
|
return Properties(self._edge)
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
def __eq__(self, other) -> bool:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Raise InvalidContextError."""
|
2020-02-25 17:24:16 +08:00
|
|
|
|
if not self.is_valid():
|
2020-03-03 16:51:15 +08:00
|
|
|
|
raise InvalidContextError()
|
2020-10-02 02:51:55 +08:00
|
|
|
|
if not isinstance(other, Edge):
|
|
|
|
|
return NotImplemented
|
2020-02-25 17:24:16 +08:00
|
|
|
|
return self._edge == other._edge
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2020-03-17 20:44:23 +08:00
|
|
|
|
def __hash__(self) -> int:
|
2020-03-18 21:07:56 +08:00
|
|
|
|
return hash(self.id)
|
2020-03-17 20:44:23 +08:00
|
|
|
|
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2020-02-27 19:24:26 +08:00
|
|
|
|
if sys.version_info >= (3, 5, 2):
|
|
|
|
|
VertexId = typing.NewType('VertexId', int)
|
|
|
|
|
else:
|
|
|
|
|
VertexId = int
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Vertex:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Vertex in the graph database.
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
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
|
2020-03-03 16:51:15 +08:00
|
|
|
|
invalid Vertex instance will raise InvalidContextError.
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
2020-02-26 20:13:36 +08:00
|
|
|
|
__slots__ = ('_vertex',)
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2020-02-12 17:27:59 +08:00
|
|
|
|
def __init__(self, vertex):
|
2020-02-26 20:13:36 +08:00
|
|
|
|
if not isinstance(vertex, _mgp.Vertex):
|
2021-07-05 17:42:40 +08:00
|
|
|
|
raise TypeError(
|
|
|
|
|
"Expected '_mgp.Vertex', got '{}'".format(type(vertex)))
|
2020-02-26 20:13:36 +08:00
|
|
|
|
self._vertex = vertex
|
|
|
|
|
|
2020-02-28 16:44:39 +08:00
|
|
|
|
def __deepcopy__(self, memo):
|
|
|
|
|
# This is the same as the shallow copy, because we want to share the
|
|
|
|
|
# underlying C struct. Besides, it doesn't make much sense to actually
|
|
|
|
|
# copy _mgp.Vertex as that is actually a reference to a graph element
|
|
|
|
|
# and not a proper value.
|
|
|
|
|
return Vertex(self._vertex)
|
|
|
|
|
|
2020-02-26 20:13:36 +08:00
|
|
|
|
def is_valid(self) -> bool:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Return True if `self` is in valid context and may be used."""
|
2020-02-26 20:13:36 +08:00
|
|
|
|
return self._vertex.is_valid()
|
2020-02-12 17:27:59 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
def underlying_graph_is_mutable(self) -> bool:
|
|
|
|
|
"""Return True if the vertex can be modified."""
|
|
|
|
|
if not self.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
return self._vertex.underlying_graph_is_mutable()
|
|
|
|
|
|
2020-02-04 16:29:38 +08:00
|
|
|
|
@property
|
|
|
|
|
def id(self) -> VertexId:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Get the ID of the vertex.
|
|
|
|
|
|
|
|
|
|
Raise InvalidContextError.
|
|
|
|
|
"""
|
2020-02-26 20:13:36 +08:00
|
|
|
|
if not self.is_valid():
|
2020-03-03 16:51:15 +08:00
|
|
|
|
raise InvalidContextError()
|
2020-02-26 20:13:36 +08:00
|
|
|
|
return self._vertex.get_id()
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
@property
|
2021-09-22 14:49:29 +08:00
|
|
|
|
def labels(self) -> typing.Tuple[Label]:
|
|
|
|
|
"""
|
|
|
|
|
Get the labels of the vertex.
|
|
|
|
|
|
|
|
|
|
Raise InvalidContextError.
|
|
|
|
|
Raise OutOfRangeError if some of the labels are removed while
|
|
|
|
|
collecting the labels.
|
|
|
|
|
Raise DeletedObjectError if `self` has been deleted.
|
|
|
|
|
"""
|
2020-02-26 20:13:36 +08:00
|
|
|
|
if not self.is_valid():
|
2020-03-03 16:51:15 +08:00
|
|
|
|
raise InvalidContextError()
|
2020-02-26 20:13:36 +08:00
|
|
|
|
return tuple(Label(self._vertex.label_at(i))
|
|
|
|
|
for i in range(self._vertex.labels_count()))
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
def add_label(self, label: str) -> None:
|
|
|
|
|
"""
|
|
|
|
|
Add the label to the vertex.
|
|
|
|
|
|
|
|
|
|
Raise InvalidContextError.
|
|
|
|
|
Raise UnableToAllocateError if unable to allocate memory for storing
|
|
|
|
|
the label.
|
|
|
|
|
Raise ImmutableObjectError if `self` is immutable.
|
|
|
|
|
Raise DeletedObjectError if `self` has been deleted.
|
|
|
|
|
Raise SerializationError if `self` has been modified by another
|
|
|
|
|
transaction.
|
|
|
|
|
"""
|
|
|
|
|
if not self.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
return self._vertex.add_label(label)
|
|
|
|
|
|
|
|
|
|
def remove_label(self, label: str) -> None:
|
|
|
|
|
"""
|
|
|
|
|
Remove the label from the vertex.
|
|
|
|
|
|
|
|
|
|
Raise InvalidContextError.
|
|
|
|
|
Raise ImmutableObjectError if `self` is immutable.
|
|
|
|
|
Raise DeletedObjectError if `self` has been deleted.
|
|
|
|
|
Raise SerializationError if `self` has been modified by another
|
|
|
|
|
transaction.
|
|
|
|
|
"""
|
|
|
|
|
if not self.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
return self._vertex.remove_label(label)
|
|
|
|
|
|
2020-02-04 16:29:38 +08:00
|
|
|
|
@property
|
|
|
|
|
def properties(self) -> Properties:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Get the properties of the vertex.
|
|
|
|
|
|
|
|
|
|
Raise InvalidContextError.
|
|
|
|
|
"""
|
2020-02-26 20:13:36 +08:00
|
|
|
|
if not self.is_valid():
|
2020-03-03 16:51:15 +08:00
|
|
|
|
raise InvalidContextError()
|
2020-02-26 20:13:36 +08:00
|
|
|
|
return Properties(self._vertex)
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def in_edges(self) -> typing.Iterable[Edge]:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Iterate over inbound edges of the vertex.
|
|
|
|
|
|
2021-09-30 21:11:51 +08:00
|
|
|
|
Doesn’t return a dynamic view of the edges, but copies the
|
|
|
|
|
current inbound edges.
|
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
Raise InvalidContextError.
|
|
|
|
|
Raise UnableToAllocateError if unable to allocate an iterator.
|
|
|
|
|
Raise DeletedObjectError if `self` has been deleted.
|
|
|
|
|
"""
|
2020-02-26 20:13:36 +08:00
|
|
|
|
if not self.is_valid():
|
2020-03-03 16:51:15 +08:00
|
|
|
|
raise InvalidContextError()
|
2020-02-27 22:13:26 +08:00
|
|
|
|
edges_it = self._vertex.iter_in_edges()
|
|
|
|
|
edge = edges_it.get()
|
|
|
|
|
while edge is not None:
|
|
|
|
|
yield Edge(edge)
|
|
|
|
|
if not self.is_valid():
|
2020-03-03 16:51:15 +08:00
|
|
|
|
raise InvalidContextError()
|
2020-02-27 22:13:26 +08:00
|
|
|
|
edge = edges_it.next()
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def out_edges(self) -> typing.Iterable[Edge]:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Iterate over outbound edges of the vertex.
|
|
|
|
|
|
2021-09-30 21:11:51 +08:00
|
|
|
|
Doesn’t return a dynamic view of the edges, but copies the
|
|
|
|
|
current outbound edges.
|
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
Raise InvalidContextError.
|
|
|
|
|
Raise UnableToAllocateError if unable to allocate an iterator.
|
|
|
|
|
Raise DeletedObjectError if `self` has been deleted.
|
|
|
|
|
"""
|
2020-02-26 20:13:36 +08:00
|
|
|
|
if not self.is_valid():
|
2020-03-03 16:51:15 +08:00
|
|
|
|
raise InvalidContextError()
|
2020-02-27 22:13:26 +08:00
|
|
|
|
edges_it = self._vertex.iter_out_edges()
|
|
|
|
|
edge = edges_it.get()
|
|
|
|
|
while edge is not None:
|
|
|
|
|
yield Edge(edge)
|
|
|
|
|
if not self.is_valid():
|
2020-03-03 16:51:15 +08:00
|
|
|
|
raise InvalidContextError()
|
2020-02-27 22:13:26 +08:00
|
|
|
|
edge = edges_it.next()
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
def __eq__(self, other) -> bool:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Raise InvalidContextError"""
|
2020-02-26 20:13:36 +08:00
|
|
|
|
if not self.is_valid():
|
2020-03-03 16:51:15 +08:00
|
|
|
|
raise InvalidContextError()
|
2020-10-02 02:51:55 +08:00
|
|
|
|
if not isinstance(other, Vertex):
|
|
|
|
|
return NotImplemented
|
2020-02-26 20:13:36 +08:00
|
|
|
|
return self._vertex == other._vertex
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2020-03-17 20:44:23 +08:00
|
|
|
|
def __hash__(self) -> int:
|
|
|
|
|
return hash(self.id)
|
|
|
|
|
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
class Path:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Path containing Vertex and Edge instances."""
|
2020-02-28 16:44:39 +08:00
|
|
|
|
__slots__ = ('_path', '_vertices', '_edges')
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2020-02-28 16:44:39 +08:00
|
|
|
|
def __init__(self, starting_vertex_or_path: typing.Union[_mgp.Path, Vertex]):
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Initialize with a starting Vertex.
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2020-03-03 16:51:15 +08:00
|
|
|
|
Raise InvalidContextError if passed in Vertex is invalid.
|
2021-09-22 14:49:29 +08:00
|
|
|
|
Raise UnableToAllocateError if cannot allocate a path.
|
|
|
|
|
"""
|
2020-02-28 16:44:39 +08:00
|
|
|
|
# We cache calls to `vertices` and `edges`, so as to avoid needless
|
|
|
|
|
# allocations at the C level.
|
|
|
|
|
self._vertices = None
|
|
|
|
|
self._edges = None
|
|
|
|
|
# Accepting _mgp.Path is just for internal usage.
|
|
|
|
|
if isinstance(starting_vertex_or_path, _mgp.Path):
|
|
|
|
|
self._path = starting_vertex_or_path
|
|
|
|
|
elif isinstance(starting_vertex_or_path, Vertex):
|
|
|
|
|
vertex = starting_vertex_or_path._vertex
|
|
|
|
|
if not vertex.is_valid():
|
2020-03-03 16:51:15 +08:00
|
|
|
|
raise InvalidContextError()
|
2020-02-28 16:44:39 +08:00
|
|
|
|
self._path = _mgp.Path.make_with_start(vertex)
|
|
|
|
|
else:
|
|
|
|
|
raise TypeError("Expected '_mgp.Vertex' or '_mgp.Path', got '{}'"
|
|
|
|
|
.format(type(starting_vertex_or_path)))
|
|
|
|
|
|
|
|
|
|
def __copy__(self):
|
|
|
|
|
if not self.is_valid():
|
2020-03-03 16:51:15 +08:00
|
|
|
|
raise InvalidContextError()
|
2020-02-28 16:44:39 +08:00
|
|
|
|
assert len(self.vertices) >= 1
|
|
|
|
|
path = Path(self.vertices[0])
|
|
|
|
|
for e in self.edges:
|
|
|
|
|
path.expand(e)
|
|
|
|
|
return path
|
|
|
|
|
|
|
|
|
|
def __deepcopy__(self, memo):
|
|
|
|
|
try:
|
|
|
|
|
return Path(memo[id(self._path)])
|
|
|
|
|
except KeyError:
|
|
|
|
|
pass
|
|
|
|
|
# This is the same as the shallow copy, as the underlying C API should
|
|
|
|
|
# not support deepcopy. Besides, it doesn't make much sense to actually
|
2020-10-02 02:51:55 +08:00
|
|
|
|
# copy _mgp.Edge and _mgp.Vertex types as they are actually references
|
|
|
|
|
# to graph elements and not proper values.
|
2020-02-28 16:44:39 +08:00
|
|
|
|
path = self.__copy__()
|
|
|
|
|
memo[id(self._path)] = path._path
|
|
|
|
|
return path
|
|
|
|
|
|
|
|
|
|
def is_valid(self) -> bool:
|
|
|
|
|
return self._path.is_valid()
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
def expand(self, edge: Edge):
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Append an edge continuing from the last vertex on the path.
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
The last vertex on the path will become the other endpoint of the given
|
|
|
|
|
edge, as continued from the current last vertex.
|
|
|
|
|
|
2020-03-03 16:51:15 +08:00
|
|
|
|
Raise InvalidContextError if using an invalid Path instance or if
|
|
|
|
|
passed in edge is invalid.
|
2021-09-22 14:49:29 +08:00
|
|
|
|
Raise LogicErrorError if the current last vertex in the path is not
|
|
|
|
|
part of the given edge.
|
|
|
|
|
Raise UnableToAllocateError if unable to allocate memory for path
|
|
|
|
|
extension.
|
|
|
|
|
"""
|
2020-02-28 16:44:39 +08:00
|
|
|
|
if not isinstance(edge, Edge):
|
2021-07-05 17:42:40 +08:00
|
|
|
|
raise TypeError(
|
|
|
|
|
"Expected '_mgp.Edge', got '{}'".format(type(edge)))
|
2020-03-03 16:51:15 +08:00
|
|
|
|
if not self.is_valid() or not edge.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
2020-02-28 16:44:39 +08:00
|
|
|
|
self._path.expand(edge._edge)
|
|
|
|
|
# Invalidate our cached tuples
|
|
|
|
|
self._vertices = None
|
|
|
|
|
self._edges = None
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def vertices(self) -> typing.Tuple[Vertex, ...]:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Vertices ordered from the start to the end of the path.
|
2020-02-28 16:44:39 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
Raise InvalidContextError if using an invalid Path instance.
|
|
|
|
|
"""
|
2020-02-28 16:44:39 +08:00
|
|
|
|
if not self.is_valid():
|
2020-03-03 16:51:15 +08:00
|
|
|
|
raise InvalidContextError()
|
2020-02-28 16:44:39 +08:00
|
|
|
|
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
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def edges(self) -> typing.Tuple[Edge, ...]:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Edges ordered from the start to the end of the path.
|
2020-02-28 16:44:39 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
Raise InvalidContextError if using an invalid Path instance.
|
|
|
|
|
"""
|
2020-02-28 16:44:39 +08:00
|
|
|
|
if not self.is_valid():
|
2020-03-03 16:51:15 +08:00
|
|
|
|
raise InvalidContextError()
|
2020-02-28 16:44:39 +08:00
|
|
|
|
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
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Record:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Represents a record of resulting field values."""
|
Register Python procedures
Summary:
With this diff you should now be able to register `example.py` and read
procedures found there. The procedures will be listed through `CALL
mg.procedures() YIELD *` query, but invoking them will raise
`NotYetImplemented`.
If you wish to test this, you will need to run the Memgraph executable
with PYTHONPATH set to the `include` directory where `mgp.py` is found.
Additionally, you need to pass `--query-modules-directory` flag to
Memgraph, such that it points to where it will find the `example.py`.
For example, when running from the root directory of Memgraph repo, the
shell invocation below should do the trick (assuming `./build/memgraph`
is where is the executable). Make sure that `./query_modules/` does not
have `example.so` built, as that may interfere with loading
`example.py`.
PYTHONPATH=$PWD/include ./build/memgraph --query-modules-directory=./query_modules/
Reviewers: mferencevic, ipaljak, llugovic
Reviewed By: mferencevic, ipaljak, llugovic
Subscribers: pullbot
Differential Revision: https://phabricator.memgraph.io/D2678
2020-02-19 17:39:18 +08:00
|
|
|
|
__slots__ = ('fields',)
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
def __init__(self, **kwargs):
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Initialize with name=value fields in kwargs."""
|
Register Python procedures
Summary:
With this diff you should now be able to register `example.py` and read
procedures found there. The procedures will be listed through `CALL
mg.procedures() YIELD *` query, but invoking them will raise
`NotYetImplemented`.
If you wish to test this, you will need to run the Memgraph executable
with PYTHONPATH set to the `include` directory where `mgp.py` is found.
Additionally, you need to pass `--query-modules-directory` flag to
Memgraph, such that it points to where it will find the `example.py`.
For example, when running from the root directory of Memgraph repo, the
shell invocation below should do the trick (assuming `./build/memgraph`
is where is the executable). Make sure that `./query_modules/` does not
have `example.so` built, as that may interfere with loading
`example.py`.
PYTHONPATH=$PWD/include ./build/memgraph --query-modules-directory=./query_modules/
Reviewers: mferencevic, ipaljak, llugovic
Reviewed By: mferencevic, ipaljak, llugovic
Subscribers: pullbot
Differential Revision: https://phabricator.memgraph.io/D2678
2020-02-19 17:39:18 +08:00
|
|
|
|
self.fields = kwargs
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Vertices:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Iterable over vertices in a graph."""
|
2020-03-19 17:05:27 +08:00
|
|
|
|
__slots__ = ('_graph', '_len')
|
2020-02-12 17:27:59 +08:00
|
|
|
|
|
|
|
|
|
def __init__(self, graph):
|
|
|
|
|
if not isinstance(graph, _mgp.Graph):
|
2021-07-05 17:42:40 +08:00
|
|
|
|
raise TypeError(
|
|
|
|
|
"Expected '_mgp.Graph', got '{}'".format(type(graph)))
|
2020-02-12 17:27:59 +08:00
|
|
|
|
self._graph = graph
|
2020-03-19 17:05:27 +08:00
|
|
|
|
self._len = None
|
2020-02-12 17:27:59 +08:00
|
|
|
|
|
2020-02-28 16:44:39 +08:00
|
|
|
|
def __deepcopy__(self, memo):
|
|
|
|
|
# This is the same as the shallow copy, because we want to share the
|
|
|
|
|
# underlying C struct. Besides, it doesn't make much sense to actually
|
|
|
|
|
# copy _mgp.Graph as that always references the whole graph state.
|
|
|
|
|
return Vertices(self._graph)
|
|
|
|
|
|
2020-02-12 17:27:59 +08:00
|
|
|
|
def is_valid(self) -> bool:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Return True if `self` is in valid context and may be used."""
|
2020-02-12 17:27:59 +08:00
|
|
|
|
return self._graph.is_valid()
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
def __iter__(self) -> typing.Iterable[Vertex]:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Iterate over vertices.
|
|
|
|
|
|
|
|
|
|
Raise InvalidContextError if context is invalid.
|
|
|
|
|
Raise UnableToAllocateError if unable to allocate an iterator or
|
|
|
|
|
a vertex.
|
|
|
|
|
"""
|
2020-02-12 17:27:59 +08:00
|
|
|
|
if not self.is_valid():
|
2020-03-03 16:51:15 +08:00
|
|
|
|
raise InvalidContextError()
|
2020-02-12 17:27:59 +08:00
|
|
|
|
vertices_it = self._graph.iter_vertices()
|
|
|
|
|
vertex = vertices_it.get()
|
|
|
|
|
while vertex is not None:
|
|
|
|
|
yield Vertex(vertex)
|
|
|
|
|
if not self.is_valid():
|
2020-03-03 16:51:15 +08:00
|
|
|
|
raise InvalidContextError()
|
2020-02-12 17:27:59 +08:00
|
|
|
|
vertex = vertices_it.next()
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2020-03-19 17:05:27 +08:00
|
|
|
|
def __contains__(self, vertex):
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Raise UnableToAllocateError if unable to allocate the vertex.
|
|
|
|
|
"""
|
2020-03-19 17:05:27 +08:00
|
|
|
|
try:
|
2020-10-02 02:51:55 +08:00
|
|
|
|
_ = self._graph.get_vertex_by_id(vertex.id)
|
2020-03-19 17:05:27 +08:00
|
|
|
|
return True
|
|
|
|
|
except IndexError:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def __len__(self):
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Get the number of vertices.
|
|
|
|
|
|
|
|
|
|
Raise InvalidContextError if context is invalid.
|
|
|
|
|
Raise UnableToAllocateError if unable to allocate an iterator or
|
|
|
|
|
a vertex.
|
|
|
|
|
"""
|
2020-03-19 17:05:27 +08:00
|
|
|
|
if not self._len:
|
|
|
|
|
self._len = sum(1 for _ in self)
|
|
|
|
|
return self._len
|
|
|
|
|
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
class Graph:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""State of the graph database in current ProcCtx."""
|
2020-02-12 17:27:59 +08:00
|
|
|
|
__slots__ = ('_graph',)
|
|
|
|
|
|
|
|
|
|
def __init__(self, graph):
|
|
|
|
|
if not isinstance(graph, _mgp.Graph):
|
2021-07-05 17:42:40 +08:00
|
|
|
|
raise TypeError(
|
|
|
|
|
"Expected '_mgp.Graph', got '{}'".format(type(graph)))
|
2020-02-12 17:27:59 +08:00
|
|
|
|
self._graph = graph
|
|
|
|
|
|
2020-02-28 16:44:39 +08:00
|
|
|
|
def __deepcopy__(self, memo):
|
|
|
|
|
# This is the same as the shallow copy, because we want to share the
|
|
|
|
|
# underlying C struct. Besides, it doesn't make much sense to actually
|
|
|
|
|
# copy _mgp.Graph as that always references the whole graph state.
|
|
|
|
|
return Graph(self._graph)
|
|
|
|
|
|
2020-02-12 17:27:59 +08:00
|
|
|
|
def is_valid(self) -> bool:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Return True if `self` is in valid context and may be used."""
|
2020-02-12 17:27:59 +08:00
|
|
|
|
return self._graph.is_valid()
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
def get_vertex_by_id(self, vertex_id: VertexId) -> Vertex:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Return the Vertex corresponding to given vertex_id from the graph.
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
Raise IndexError if unable to find the given vertex_id.
|
2020-03-03 16:51:15 +08:00
|
|
|
|
Raise InvalidContextError if context is invalid.
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
2020-02-12 17:27:59 +08:00
|
|
|
|
if not self.is_valid():
|
2020-03-03 16:51:15 +08:00
|
|
|
|
raise InvalidContextError()
|
2020-02-12 17:27:59 +08:00
|
|
|
|
vertex = self._graph.get_vertex_by_id(vertex_id)
|
|
|
|
|
return Vertex(vertex)
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def vertices(self) -> Vertices:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
All vertices in the graph.
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
instances.
|
|
|
|
|
|
2020-03-03 16:51:15 +08:00
|
|
|
|
Raise InvalidContextError if context is invalid.
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
2020-02-12 17:27:59 +08:00
|
|
|
|
if not self.is_valid():
|
2020-03-03 16:51:15 +08:00
|
|
|
|
raise InvalidContextError()
|
2020-02-12 17:27:59 +08:00
|
|
|
|
return Vertices(self._graph)
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
def is_mutable(self) -> bool:
|
|
|
|
|
"""
|
|
|
|
|
Return True if `self` represents a mutable graph, thus it can be
|
|
|
|
|
used to modify vertices and edges.
|
|
|
|
|
"""
|
|
|
|
|
if not self.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
return self._graph.is_mutable()
|
|
|
|
|
|
|
|
|
|
def create_vertex(self) -> Vertex:
|
|
|
|
|
"""
|
|
|
|
|
Create a vertex.
|
|
|
|
|
|
|
|
|
|
Raise ImmutableObjectError if `self` is immutable.
|
|
|
|
|
Raise UnableToAllocateError if unable to allocate a vertex.
|
|
|
|
|
"""
|
|
|
|
|
if not self.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
return Vertex(self._graph.create_vertex())
|
|
|
|
|
|
|
|
|
|
def delete_vertex(self, vertex: Vertex) -> None:
|
|
|
|
|
"""
|
|
|
|
|
Delete a vertex.
|
|
|
|
|
|
|
|
|
|
Raise ImmutableObjectError if `self` is immutable.
|
|
|
|
|
Raise LogicErrorError if `vertex` has edges.
|
|
|
|
|
Raise SerializationError if `vertex` has been modified by
|
|
|
|
|
another transaction.
|
|
|
|
|
"""
|
|
|
|
|
if not self.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
self._graph.delete_vertex(vertex._vertex)
|
|
|
|
|
|
|
|
|
|
def detach_delete_vertex(self, vertex: Vertex) -> None:
|
|
|
|
|
"""
|
|
|
|
|
Delete a vertex and all of its edges.
|
|
|
|
|
|
|
|
|
|
Raise ImmutableObjectError if `self` is immutable.
|
|
|
|
|
Raise SerializationError if `vertex` has been modified by
|
|
|
|
|
another transaction.
|
|
|
|
|
"""
|
|
|
|
|
if not self.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
self._graph.detach_delete_vertex(vertex._vertex)
|
|
|
|
|
|
|
|
|
|
def create_edge(self, from_vertex: Vertex, to_vertex: Vertex,
|
|
|
|
|
edge_type: EdgeType) -> None:
|
|
|
|
|
"""
|
|
|
|
|
Create an edge.
|
|
|
|
|
|
|
|
|
|
Raise ImmutableObjectError if `self ` is immutable.
|
|
|
|
|
Raise UnableToAllocateError if unable to allocate an edge.
|
|
|
|
|
Raise DeletedObjectError if `from_vertex` or `to_vertex` has
|
|
|
|
|
been deleted.
|
|
|
|
|
Raise SerializationError if `from_vertex` or `to_vertex` has
|
|
|
|
|
been modified by another transaction.
|
|
|
|
|
"""
|
|
|
|
|
if not self.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
return Edge(self._graph.create_edge(from_vertex._vertex,
|
|
|
|
|
to_vertex._vertex, edge_type.name))
|
|
|
|
|
|
|
|
|
|
def delete_edge(self, edge: Edge) -> None:
|
|
|
|
|
"""
|
|
|
|
|
Delete an edge.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Raise ImmutableObjectError if `self` is immutable.
|
|
|
|
|
Raise SerializationError if `edge`, its source or destination
|
|
|
|
|
vertex has been modified by another transaction.
|
|
|
|
|
"""
|
|
|
|
|
if not self.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
self._graph.delete_edge(edge._edge)
|
|
|
|
|
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2020-04-01 06:07:32 +08:00
|
|
|
|
class AbortError(Exception):
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Signals that the procedure was asked to abort its execution."""
|
2020-04-01 06:07:32 +08:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2020-02-04 16:29:38 +08:00
|
|
|
|
class ProcCtx:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Context of a procedure being executed.
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
Access to a ProcCtx is only valid during a single execution of a procedure
|
|
|
|
|
in a query. You should not globally store a ProcCtx instance.
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
Implement calling Python procedures
Summary:
You should now be able to invoke query procedures written in Python. To
test the example you can run memgraph with PYTHONPATH set to `include`.
For example, assuming you are in the root of the repo, run this command.
PYTHONPATH=$PWD/include ./build/memgraph --query-modules-directory=./query_modules/
Alternatively, you can set a symlink inside the ./query_modules to point
to `include/mgp.py`, so there's no need to set PYTHONPATH. For example,
assuming you are in the root of the repo, run the following.
cd ./query_modules
ln -s ../include/mgp.py
cd ..
./build/memgraph --query-modules-directory=./query_modules/
Depends on D207
Reviewers: mferencevic, ipaljak, dsantl
Reviewed By: ipaljak
Subscribers: buda, tlastre, pullbot
Differential Revision: https://phabricator.memgraph.io/D2708
2020-03-06 22:31:31 +08:00
|
|
|
|
__slots__ = ('_graph',)
|
|
|
|
|
|
|
|
|
|
def __init__(self, graph):
|
|
|
|
|
if not isinstance(graph, _mgp.Graph):
|
2021-07-05 17:42:40 +08:00
|
|
|
|
raise TypeError(
|
|
|
|
|
"Expected '_mgp.Graph', got '{}'".format(type(graph)))
|
Implement calling Python procedures
Summary:
You should now be able to invoke query procedures written in Python. To
test the example you can run memgraph with PYTHONPATH set to `include`.
For example, assuming you are in the root of the repo, run this command.
PYTHONPATH=$PWD/include ./build/memgraph --query-modules-directory=./query_modules/
Alternatively, you can set a symlink inside the ./query_modules to point
to `include/mgp.py`, so there's no need to set PYTHONPATH. For example,
assuming you are in the root of the repo, run the following.
cd ./query_modules
ln -s ../include/mgp.py
cd ..
./build/memgraph --query-modules-directory=./query_modules/
Depends on D207
Reviewers: mferencevic, ipaljak, dsantl
Reviewed By: ipaljak
Subscribers: buda, tlastre, pullbot
Differential Revision: https://phabricator.memgraph.io/D2708
2020-03-06 22:31:31 +08:00
|
|
|
|
self._graph = Graph(graph)
|
|
|
|
|
|
|
|
|
|
def is_valid(self) -> bool:
|
|
|
|
|
return self._graph.is_valid()
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def graph(self) -> Graph:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Raise InvalidContextError if context is invalid."""
|
Implement calling Python procedures
Summary:
You should now be able to invoke query procedures written in Python. To
test the example you can run memgraph with PYTHONPATH set to `include`.
For example, assuming you are in the root of the repo, run this command.
PYTHONPATH=$PWD/include ./build/memgraph --query-modules-directory=./query_modules/
Alternatively, you can set a symlink inside the ./query_modules to point
to `include/mgp.py`, so there's no need to set PYTHONPATH. For example,
assuming you are in the root of the repo, run the following.
cd ./query_modules
ln -s ../include/mgp.py
cd ..
./build/memgraph --query-modules-directory=./query_modules/
Depends on D207
Reviewers: mferencevic, ipaljak, dsantl
Reviewed By: ipaljak
Subscribers: buda, tlastre, pullbot
Differential Revision: https://phabricator.memgraph.io/D2708
2020-03-06 22:31:31 +08:00
|
|
|
|
if not self.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
return self._graph
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2020-04-01 06:07:32 +08:00
|
|
|
|
def must_abort(self) -> bool:
|
|
|
|
|
if not self.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
return self._graph._graph.must_abort()
|
|
|
|
|
|
|
|
|
|
def check_must_abort(self):
|
|
|
|
|
if self.must_abort():
|
|
|
|
|
raise AbortError
|
|
|
|
|
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
# Additional typing support
|
|
|
|
|
|
|
|
|
|
Number = typing.Union[int, float]
|
|
|
|
|
|
|
|
|
|
Map = typing.Union[dict, Edge, Vertex]
|
|
|
|
|
|
2021-09-20 16:59:30 +08:00
|
|
|
|
Date = datetime.date
|
|
|
|
|
|
|
|
|
|
LocalTime = datetime.time
|
|
|
|
|
|
|
|
|
|
LocalDateTime = datetime.datetime
|
|
|
|
|
|
|
|
|
|
Duration = datetime.timedelta
|
|
|
|
|
|
2021-10-03 17:09:57 +08:00
|
|
|
|
Any = typing.Union[bool, str, Number, Map, Path,
|
|
|
|
|
list, Date, LocalTime, LocalDateTime, Duration]
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2020-02-27 19:24:26 +08:00
|
|
|
|
List = typing.List
|
|
|
|
|
|
2020-02-04 16:29:38 +08:00
|
|
|
|
Nullable = typing.Optional
|
|
|
|
|
|
|
|
|
|
|
2020-02-27 19:24:26 +08:00
|
|
|
|
class UnsupportedTypingError(Exception):
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Signals a typing annotation is not supported as a _mgp.CypherType."""
|
2020-02-27 19:24:26 +08:00
|
|
|
|
|
|
|
|
|
def __init__(self, type_):
|
|
|
|
|
super().__init__("Unsupported typing annotation '{}'".format(type_))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _typing_to_cypher_type(type_):
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Convert typing annotation to a _mgp.CypherType instance."""
|
2020-02-27 19:24:26 +08:00
|
|
|
|
simple_types = {
|
|
|
|
|
typing.Any: _mgp.type_nullable(_mgp.type_any()),
|
|
|
|
|
object: _mgp.type_nullable(_mgp.type_any()),
|
|
|
|
|
list: _mgp.type_list(_mgp.type_nullable(_mgp.type_any())),
|
|
|
|
|
Any: _mgp.type_any(),
|
|
|
|
|
bool: _mgp.type_bool(),
|
|
|
|
|
str: _mgp.type_string(),
|
|
|
|
|
int: _mgp.type_int(),
|
|
|
|
|
float: _mgp.type_float(),
|
|
|
|
|
Number: _mgp.type_number(),
|
|
|
|
|
Map: _mgp.type_map(),
|
|
|
|
|
Vertex: _mgp.type_node(),
|
|
|
|
|
Edge: _mgp.type_relationship(),
|
2021-09-20 16:59:30 +08:00
|
|
|
|
Path: _mgp.type_path(),
|
|
|
|
|
Date: _mgp.type_date(),
|
|
|
|
|
LocalTime: _mgp.type_local_time(),
|
|
|
|
|
LocalDateTime: _mgp.type_local_date_time(),
|
|
|
|
|
Duration: _mgp.type_duration()
|
2020-02-27 19:24:26 +08:00
|
|
|
|
}
|
|
|
|
|
try:
|
|
|
|
|
return simple_types[type_]
|
|
|
|
|
except KeyError:
|
|
|
|
|
pass
|
|
|
|
|
if sys.version_info >= (3, 8):
|
|
|
|
|
complex_type = typing.get_origin(type_)
|
|
|
|
|
type_args = typing.get_args(type_)
|
|
|
|
|
if complex_type == typing.Union:
|
|
|
|
|
# If we have a Union with NoneType inside, it means we are building
|
|
|
|
|
# a nullable type.
|
2021-06-10 01:09:43 +08:00
|
|
|
|
# isinstance doesn't work here because subscripted generics cannot
|
|
|
|
|
# be used with class and instance checks. type comparison should be
|
|
|
|
|
# fine because subclasses are not used.
|
|
|
|
|
if type(None) in type_args:
|
|
|
|
|
types = tuple(t for t in type_args if t is not type(None)) # noqa E721
|
2020-02-27 19:24:26 +08:00
|
|
|
|
if len(types) == 1:
|
|
|
|
|
type_arg, = types
|
|
|
|
|
else:
|
|
|
|
|
# We cannot do typing.Union[*types], so do the equivalent
|
|
|
|
|
# with __getitem__ which does not even need arg unpacking.
|
|
|
|
|
type_arg = typing.Union.__getitem__(types)
|
|
|
|
|
return _mgp.type_nullable(_typing_to_cypher_type(type_arg))
|
|
|
|
|
elif complex_type == list:
|
|
|
|
|
type_arg, = type_args
|
|
|
|
|
return _mgp.type_list(_typing_to_cypher_type(type_arg))
|
|
|
|
|
raise UnsupportedTypingError(type_)
|
|
|
|
|
else:
|
|
|
|
|
# We cannot get to type args in any reliable way prior to 3.8, but we
|
|
|
|
|
# still want to support typing.Optional and typing.List, so just parse
|
|
|
|
|
# their string representations. Hopefully, that is always pretty
|
|
|
|
|
# printed the same way. `typing.List[type]` is printed as such, while
|
|
|
|
|
# `typing.Optional[type]` is printed as 'typing.Union[type, NoneType]'
|
|
|
|
|
def parse_type_args(type_as_str):
|
|
|
|
|
return tuple(map(str.strip,
|
|
|
|
|
type_as_str[type_as_str.index('[') + 1: -1].split(',')))
|
|
|
|
|
|
2020-03-12 20:51:59 +08:00
|
|
|
|
def fully_qualified_name(cls):
|
|
|
|
|
if cls.__module__ is None or cls.__module__ == 'builtins':
|
|
|
|
|
return cls.__name__
|
|
|
|
|
return cls.__module__ + '.' + cls.__name__
|
|
|
|
|
|
2020-02-27 19:24:26 +08:00
|
|
|
|
def get_simple_type(type_as_str):
|
|
|
|
|
for simple_type, cypher_type in simple_types.items():
|
|
|
|
|
if type_as_str == str(simple_type):
|
|
|
|
|
return cypher_type
|
|
|
|
|
# Fallback to comparing to __name__ if it exits. This handles
|
|
|
|
|
# the cases like when we have 'object' which is
|
|
|
|
|
# `object.__name__`, but `str(object)` is "<class 'object'>"
|
|
|
|
|
try:
|
2020-03-12 20:51:59 +08:00
|
|
|
|
if type_as_str == fully_qualified_name(simple_type):
|
2020-02-27 19:24:26 +08:00
|
|
|
|
return cypher_type
|
|
|
|
|
except AttributeError:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def parse_typing(type_as_str):
|
|
|
|
|
if type_as_str.startswith('typing.Union'):
|
|
|
|
|
type_args_as_str = parse_type_args(type_as_str)
|
|
|
|
|
none_type_as_str = type(None).__name__
|
|
|
|
|
if none_type_as_str in type_args_as_str:
|
2021-07-05 17:42:40 +08:00
|
|
|
|
types = tuple(
|
|
|
|
|
t for t in type_args_as_str if t != none_type_as_str)
|
2020-02-27 19:24:26 +08:00
|
|
|
|
if len(types) == 1:
|
|
|
|
|
type_arg_as_str, = types
|
|
|
|
|
else:
|
2021-07-05 17:42:40 +08:00
|
|
|
|
type_arg_as_str = 'typing.Union[' + \
|
|
|
|
|
', '.join(types) + ']'
|
2020-02-27 19:24:26 +08:00
|
|
|
|
simple_type = get_simple_type(type_arg_as_str)
|
|
|
|
|
if simple_type is not None:
|
|
|
|
|
return _mgp.type_nullable(simple_type)
|
|
|
|
|
return _mgp.type_nullable(parse_typing(type_arg_as_str))
|
|
|
|
|
elif type_as_str.startswith('typing.List'):
|
2021-04-27 14:31:18 +08:00
|
|
|
|
type_arg_as_str = parse_type_args(type_as_str)
|
|
|
|
|
|
|
|
|
|
if len(type_arg_as_str) > 1:
|
|
|
|
|
# Nested object could be a type consisting of a list of types (e.g. mgp.Map)
|
|
|
|
|
# so we need to join the parts.
|
|
|
|
|
type_arg_as_str = ', '.join(type_arg_as_str)
|
|
|
|
|
else:
|
|
|
|
|
type_arg_as_str = type_arg_as_str[0]
|
|
|
|
|
|
2020-02-27 19:24:26 +08:00
|
|
|
|
simple_type = get_simple_type(type_arg_as_str)
|
|
|
|
|
if simple_type is not None:
|
|
|
|
|
return _mgp.type_list(simple_type)
|
|
|
|
|
return _mgp.type_list(parse_typing(type_arg_as_str))
|
|
|
|
|
raise UnsupportedTypingError(type_)
|
|
|
|
|
|
|
|
|
|
return parse_typing(str(type_))
|
|
|
|
|
|
|
|
|
|
|
2020-02-04 16:29:38 +08:00
|
|
|
|
# Procedure registration
|
|
|
|
|
|
|
|
|
|
class Deprecated:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Annotate a resulting Record's field as deprecated."""
|
Register Python procedures
Summary:
With this diff you should now be able to register `example.py` and read
procedures found there. The procedures will be listed through `CALL
mg.procedures() YIELD *` query, but invoking them will raise
`NotYetImplemented`.
If you wish to test this, you will need to run the Memgraph executable
with PYTHONPATH set to the `include` directory where `mgp.py` is found.
Additionally, you need to pass `--query-modules-directory` flag to
Memgraph, such that it points to where it will find the `example.py`.
For example, when running from the root directory of Memgraph repo, the
shell invocation below should do the trick (assuming `./build/memgraph`
is where is the executable). Make sure that `./query_modules/` does not
have `example.so` built, as that may interfere with loading
`example.py`.
PYTHONPATH=$PWD/include ./build/memgraph --query-modules-directory=./query_modules/
Reviewers: mferencevic, ipaljak, llugovic
Reviewed By: mferencevic, ipaljak, llugovic
Subscribers: pullbot
Differential Revision: https://phabricator.memgraph.io/D2678
2020-02-19 17:39:18 +08:00
|
|
|
|
__slots__ = ('field_type',)
|
|
|
|
|
|
2020-02-04 16:29:38 +08:00
|
|
|
|
def __init__(self, type_):
|
Register Python procedures
Summary:
With this diff you should now be able to register `example.py` and read
procedures found there. The procedures will be listed through `CALL
mg.procedures() YIELD *` query, but invoking them will raise
`NotYetImplemented`.
If you wish to test this, you will need to run the Memgraph executable
with PYTHONPATH set to the `include` directory where `mgp.py` is found.
Additionally, you need to pass `--query-modules-directory` flag to
Memgraph, such that it points to where it will find the `example.py`.
For example, when running from the root directory of Memgraph repo, the
shell invocation below should do the trick (assuming `./build/memgraph`
is where is the executable). Make sure that `./query_modules/` does not
have `example.so` built, as that may interfere with loading
`example.py`.
PYTHONPATH=$PWD/include ./build/memgraph --query-modules-directory=./query_modules/
Reviewers: mferencevic, ipaljak, llugovic
Reviewed By: mferencevic, ipaljak, llugovic
Subscribers: pullbot
Differential Revision: https://phabricator.memgraph.io/D2678
2020-02-19 17:39:18 +08:00
|
|
|
|
self.field_type = type_
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
|
2021-07-01 18:36:41 +08:00
|
|
|
|
def raise_if_does_not_meet_requirements(func: typing.Callable[..., Record]):
|
|
|
|
|
if not callable(func):
|
|
|
|
|
raise TypeError("Expected a callable object, got an instance of '{}'"
|
|
|
|
|
.format(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")
|
|
|
|
|
|
2021-07-05 17:42:40 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
def _register_proc(func: typing.Callable[..., Record],
|
|
|
|
|
is_write: bool):
|
|
|
|
|
raise_if_does_not_meet_requirements(func)
|
|
|
|
|
register_func = (
|
|
|
|
|
_mgp.Module.add_write_procedure if is_write
|
|
|
|
|
else _mgp.Module.add_read_procedure)
|
|
|
|
|
sig = inspect.signature(func)
|
|
|
|
|
params = tuple(sig.parameters.values())
|
|
|
|
|
if params and params[0].annotation is ProcCtx:
|
|
|
|
|
@wraps(func)
|
|
|
|
|
def wrapper(graph, args):
|
|
|
|
|
return func(ProcCtx(graph), *args)
|
|
|
|
|
params = params[1:]
|
|
|
|
|
mgp_proc = register_func(_mgp._MODULE, wrapper)
|
|
|
|
|
else:
|
|
|
|
|
@wraps(func)
|
|
|
|
|
def wrapper(graph, args):
|
|
|
|
|
return func(*args)
|
|
|
|
|
mgp_proc = register_func(_mgp._MODULE, wrapper)
|
|
|
|
|
for param in params:
|
|
|
|
|
name = param.name
|
|
|
|
|
type_ = param.annotation
|
|
|
|
|
if type_ is param.empty:
|
|
|
|
|
type_ = object
|
|
|
|
|
cypher_type = _typing_to_cypher_type(type_)
|
|
|
|
|
if param.default is param.empty:
|
|
|
|
|
mgp_proc.add_arg(name, cypher_type)
|
|
|
|
|
else:
|
|
|
|
|
mgp_proc.add_opt_arg(name, cypher_type, param.default)
|
|
|
|
|
if sig.return_annotation is not sig.empty:
|
|
|
|
|
record = sig.return_annotation
|
|
|
|
|
if not isinstance(record, Record):
|
|
|
|
|
raise TypeError("Expected '{}' to return 'mgp.Record', got '{}'"
|
|
|
|
|
.format(func.__name__, type(record)))
|
|
|
|
|
for name, type_ in record.fields.items():
|
|
|
|
|
if isinstance(type_, Deprecated):
|
|
|
|
|
cypher_type = _typing_to_cypher_type(type_.field_type)
|
|
|
|
|
mgp_proc.add_deprecated_result(name, cypher_type)
|
|
|
|
|
else:
|
|
|
|
|
mgp_proc.add_result(name, _typing_to_cypher_type(type_))
|
|
|
|
|
return func
|
|
|
|
|
|
|
|
|
|
|
2020-02-04 16:29:38 +08:00
|
|
|
|
def read_proc(func: typing.Callable[..., Record]):
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Register `func` as a read-only procedure of the current module.
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
`read_proc` is meant to be used as a decorator function to register module
|
|
|
|
|
procedures. The registered `func` needs to be a callable which optionally
|
|
|
|
|
takes `ProcCtx` as the first argument. Other arguments of `func` will be
|
|
|
|
|
bound to values passed in the cypherQuery. 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. To mark a field as deprecated, use
|
|
|
|
|
`Record(field_name=Deprecated(type), ...)`. Multiple records can be
|
|
|
|
|
produced by returning an iterable of them. Registering generator functions
|
|
|
|
|
is currently not supported.
|
|
|
|
|
|
|
|
|
|
Example usage.
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
import mgp
|
|
|
|
|
|
|
|
|
|
@mgp.read_proc
|
|
|
|
|
def procedure(context: mgp.ProcCtx,
|
|
|
|
|
required_arg: mgp.Nullable[mgp.Any],
|
|
|
|
|
optional_arg: mgp.Nullable[mgp.Any] = None
|
|
|
|
|
) -> mgp.Record(result=str, args=list):
|
|
|
|
|
args = [required_arg, optional_arg]
|
|
|
|
|
# Multiple rows can be produced by returning an iterable of mgp.Record
|
|
|
|
|
return mgp.Record(args=args, result='Hello World!')
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The example procedure above returns 2 fields: `args` and `result`.
|
|
|
|
|
* `args` is a copy of arguments passed to the procedure.
|
|
|
|
|
* `result` is the result of this procedure, a "Hello World!" string.
|
|
|
|
|
Any errors can be reported by raising an Exception.
|
|
|
|
|
|
|
|
|
|
The procedure can be invoked in openCypher using the following calls:
|
|
|
|
|
CALL example.procedure(1, 2) YIELD args, result;
|
|
|
|
|
CALL example.procedure(1) YIELD args, result;
|
|
|
|
|
Naturally, you may pass in different arguments or yield less fields.
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
return _register_proc(func, False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def write_proc(func: typing.Callable[..., Record]):
|
|
|
|
|
"""
|
|
|
|
|
Register `func` as a writeable procedure of the current module.
|
|
|
|
|
|
|
|
|
|
`write_proc` is meant to be used as a decorator function to register module
|
|
|
|
|
procedures. The registered `func` needs to be a callable which optionally
|
|
|
|
|
takes `ProcCtx` as the first argument. Other arguments of `func` will be
|
|
|
|
|
bound to values passed in the cypherQuery. 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. To mark a field as deprecated, use
|
|
|
|
|
`Record(field_name=Deprecated(type), ...)`. Multiple records can be
|
|
|
|
|
produced by returning an iterable of them. Registering generator functions
|
|
|
|
|
is currently not supported.
|
|
|
|
|
|
|
|
|
|
Example usage.
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
import mgp
|
|
|
|
|
|
|
|
|
|
@mgp.write_proc
|
|
|
|
|
def procedure(context: mgp.ProcCtx,
|
2021-10-02 19:09:00 +08:00
|
|
|
|
required_arg: str,
|
|
|
|
|
optional_arg: mgp.Nullable[str] = None
|
|
|
|
|
) -> mgp.Record(result=mgp.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)
|
2021-09-22 14:49:29 +08:00
|
|
|
|
```
|
|
|
|
|
|
2021-10-02 19:09:00 +08:00
|
|
|
|
The example procedure above returns a newly created vertex which has
|
|
|
|
|
at most 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`.
|
2021-09-22 14:49:29 +08:00
|
|
|
|
Any errors can be reported by raising an Exception.
|
|
|
|
|
|
|
|
|
|
The procedure can be invoked in openCypher using the following calls:
|
2021-10-02 19:09:00 +08:00
|
|
|
|
CALL example.procedure("property value", "another one") YIELD result;
|
|
|
|
|
CALL example.procedure("single argument") YIELD result;
|
|
|
|
|
Naturally, you may pass in different arguments.
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
return _register_proc(func, True)
|
2021-07-01 18:36:41 +08:00
|
|
|
|
|
2021-07-05 17:42:40 +08:00
|
|
|
|
|
2021-07-01 18:36:41 +08:00
|
|
|
|
class InvalidMessageError(Exception):
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Signals using a message instance outside of the registered transformation.
|
|
|
|
|
"""
|
2021-07-01 18:36:41 +08:00
|
|
|
|
pass
|
|
|
|
|
|
2021-07-05 17:42:40 +08:00
|
|
|
|
|
2021-11-16 22:10:49 +08:00
|
|
|
|
SOURCE_TYPE_KAFKA = _mgp.SOURCE_TYPE_KAFKA
|
|
|
|
|
SOURCE_TYPE_PULSAR = _mgp.SOURCE_TYPE_PULSAR
|
|
|
|
|
|
2021-07-05 17:42:40 +08:00
|
|
|
|
class Message:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Represents a message from a stream."""
|
2021-07-01 18:36:41 +08:00
|
|
|
|
__slots__ = ('_message',)
|
|
|
|
|
|
|
|
|
|
def __init__(self, message):
|
|
|
|
|
if not isinstance(message, _mgp.Message):
|
2021-07-05 17:42:40 +08:00
|
|
|
|
raise TypeError(
|
|
|
|
|
"Expected '_mgp.Message', got '{}'".format(type(message)))
|
2021-07-01 18:36:41 +08:00
|
|
|
|
self._message = message
|
|
|
|
|
|
|
|
|
|
def __deepcopy__(self, memo):
|
|
|
|
|
# This is the same as the shallow copy, because we want to share the
|
|
|
|
|
# underlying C struct. Besides, it doesn't make much sense to actually
|
|
|
|
|
# copy _mgp.Messages as that always references all the messages.
|
|
|
|
|
return Message(self._message)
|
|
|
|
|
|
|
|
|
|
def is_valid(self) -> bool:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Return True if `self` is in valid context and may be used."""
|
2021-07-01 18:36:41 +08:00
|
|
|
|
return self._message.is_valid()
|
|
|
|
|
|
2021-11-16 22:10:49 +08:00
|
|
|
|
def source_type(self) -> str:
|
|
|
|
|
"""
|
|
|
|
|
Supported in all stream sources
|
|
|
|
|
|
|
|
|
|
Raise InvalidArgumentError if the message is from an unsupported stream source.
|
|
|
|
|
"""
|
|
|
|
|
if not self.is_valid():
|
|
|
|
|
raise InvalidMessageError()
|
|
|
|
|
return self._message.source_type()
|
|
|
|
|
|
2021-07-01 18:36:41 +08:00
|
|
|
|
def payload(self) -> bytes:
|
2021-11-16 22:10:49 +08:00
|
|
|
|
"""
|
|
|
|
|
Supported stream sources:
|
|
|
|
|
- Kafka
|
|
|
|
|
- Pulsar
|
|
|
|
|
|
|
|
|
|
Raise InvalidArgumentError if the message is from an unsupported stream source.
|
|
|
|
|
"""
|
2021-07-01 18:36:41 +08:00
|
|
|
|
if not self.is_valid():
|
|
|
|
|
raise InvalidMessageError()
|
2021-07-05 17:42:40 +08:00
|
|
|
|
return self._message.payload()
|
2021-07-01 18:36:41 +08:00
|
|
|
|
|
|
|
|
|
def topic_name(self) -> str:
|
2021-11-16 22:10:49 +08:00
|
|
|
|
"""
|
|
|
|
|
Supported stream sources:
|
|
|
|
|
- Kafka
|
|
|
|
|
- Pulsar
|
|
|
|
|
|
|
|
|
|
Raise InvalidArgumentError if the message is from an unsupported stream source.
|
|
|
|
|
"""
|
2021-07-01 18:36:41 +08:00
|
|
|
|
if not self.is_valid():
|
|
|
|
|
raise InvalidMessageError()
|
2021-07-05 17:42:40 +08:00
|
|
|
|
return self._message.topic_name()
|
2021-07-01 18:36:41 +08:00
|
|
|
|
|
2021-07-05 17:42:40 +08:00
|
|
|
|
def key(self) -> bytes:
|
2021-11-16 22:10:49 +08:00
|
|
|
|
"""
|
|
|
|
|
Supported stream sources:
|
|
|
|
|
- Kafka
|
|
|
|
|
|
|
|
|
|
Raise InvalidArgumentError if the message is from an unsupported stream source.
|
|
|
|
|
"""
|
2021-07-01 18:36:41 +08:00
|
|
|
|
if not self.is_valid():
|
|
|
|
|
raise InvalidMessageError()
|
2021-07-05 17:42:40 +08:00
|
|
|
|
return self._message.key()
|
|
|
|
|
|
|
|
|
|
def timestamp(self) -> int:
|
2021-11-16 22:10:49 +08:00
|
|
|
|
"""
|
|
|
|
|
Supported stream sources:
|
|
|
|
|
- Kafka
|
|
|
|
|
|
|
|
|
|
Raise InvalidArgumentError if the message is from an unsupported stream source.
|
|
|
|
|
"""
|
2021-07-01 18:36:41 +08:00
|
|
|
|
if not self.is_valid():
|
|
|
|
|
raise InvalidMessageError()
|
2021-07-05 17:42:40 +08:00
|
|
|
|
return self._message.timestamp()
|
|
|
|
|
|
2021-11-11 19:07:58 +08:00
|
|
|
|
def offset(self) -> int:
|
|
|
|
|
if not self.is_valid():
|
|
|
|
|
raise InvalidMessageError()
|
|
|
|
|
return self._message.offset()
|
|
|
|
|
|
2021-07-01 18:36:41 +08:00
|
|
|
|
|
|
|
|
|
class InvalidMessagesError(Exception):
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Signals using a messages instance outside of the registered transformation."""
|
2021-07-01 18:36:41 +08:00
|
|
|
|
pass
|
|
|
|
|
|
2021-07-05 17:42:40 +08:00
|
|
|
|
|
2021-07-01 18:36:41 +08:00
|
|
|
|
class Messages:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Represents a list of messages from a stream."""
|
2021-07-01 18:36:41 +08:00
|
|
|
|
__slots__ = ('_messages',)
|
|
|
|
|
|
|
|
|
|
def __init__(self, messages):
|
|
|
|
|
if not isinstance(messages, _mgp.Messages):
|
2021-07-05 17:42:40 +08:00
|
|
|
|
raise TypeError(
|
|
|
|
|
"Expected '_mgp.Messages', got '{}'".format(type(messages)))
|
2021-07-01 18:36:41 +08:00
|
|
|
|
self._messages = messages
|
|
|
|
|
|
|
|
|
|
def __deepcopy__(self, memo):
|
|
|
|
|
# This is the same as the shallow copy, because we want to share the
|
|
|
|
|
# underlying C struct. Besides, it doesn't make much sense to actually
|
|
|
|
|
# copy _mgp.Messages as that always references all the messages.
|
|
|
|
|
return Messages(self._messages)
|
|
|
|
|
|
|
|
|
|
def is_valid(self) -> bool:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Return True if `self` is in valid context and may be used."""
|
2021-07-01 18:36:41 +08:00
|
|
|
|
return self._messages.is_valid()
|
|
|
|
|
|
2021-07-05 17:42:40 +08:00
|
|
|
|
def message_at(self, id: int) -> Message:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Raise InvalidMessagesError if context is invalid."""
|
2021-07-01 18:36:41 +08:00
|
|
|
|
if not self.is_valid():
|
|
|
|
|
raise InvalidMessagesError()
|
|
|
|
|
return Message(self._messages.message_at(id))
|
|
|
|
|
|
2021-07-05 17:42:40 +08:00
|
|
|
|
def total_messages(self) -> int:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Raise InvalidContextError if context is invalid."""
|
2021-07-01 18:36:41 +08:00
|
|
|
|
if not self.is_valid():
|
|
|
|
|
raise InvalidMessagesError()
|
|
|
|
|
return self._messages.total_messages()
|
|
|
|
|
|
2021-07-05 17:42:40 +08:00
|
|
|
|
|
2021-07-01 18:36:41 +08:00
|
|
|
|
class TransCtx:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Context of a transformation being executed.
|
2021-07-01 18:36:41 +08:00
|
|
|
|
|
|
|
|
|
Access to a TransCtx is only valid during a single execution of a transformation.
|
|
|
|
|
You should not globally store a TransCtx instance.
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
2021-07-01 18:36:41 +08:00
|
|
|
|
__slots__ = ('_graph')
|
|
|
|
|
|
|
|
|
|
def __init__(self, graph):
|
|
|
|
|
if not isinstance(graph, _mgp.Graph):
|
2021-07-05 17:42:40 +08:00
|
|
|
|
raise TypeError(
|
|
|
|
|
"Expected '_mgp.Graph', got '{}'".format(type(graph)))
|
|
|
|
|
self._graph = Graph(graph)
|
2021-07-01 18:36:41 +08:00
|
|
|
|
|
|
|
|
|
def is_valid(self) -> bool:
|
|
|
|
|
return self._graph.is_valid()
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def graph(self) -> Graph:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Raise InvalidContextError if context is invalid."""
|
2021-07-01 18:36:41 +08:00
|
|
|
|
if not self.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
return self._graph
|
|
|
|
|
|
2021-07-05 17:42:40 +08:00
|
|
|
|
|
2021-07-01 18:36:41 +08:00
|
|
|
|
def transformation(func: typing.Callable[..., Record]):
|
|
|
|
|
raise_if_does_not_meet_requirements(func)
|
|
|
|
|
sig = inspect.signature(func)
|
|
|
|
|
params = tuple(sig.parameters.values())
|
|
|
|
|
if not params or not params[0].annotation is Messages:
|
|
|
|
|
if not len(params) == 2 or not params[1].annotation is Messages:
|
2021-07-05 17:42:40 +08:00
|
|
|
|
raise NotImplementedError(
|
|
|
|
|
"Valid signatures for transformations are (TransCtx, Messages) or (Messages)")
|
2021-07-01 18:36:41 +08:00
|
|
|
|
if params[0].annotation is TransCtx:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
@wraps(func)
|
2021-07-01 18:36:41 +08:00
|
|
|
|
def wrapper(graph, messages):
|
|
|
|
|
return func(TransCtx(graph), messages)
|
|
|
|
|
_mgp._MODULE.add_transformation(wrapper)
|
|
|
|
|
else:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
@wraps(func)
|
2021-07-01 18:36:41 +08:00
|
|
|
|
def wrapper(graph, messages):
|
|
|
|
|
return func(messages)
|
|
|
|
|
_mgp._MODULE.add_transformation(wrapper)
|
|
|
|
|
return func
|
2021-09-22 14:49:29 +08:00
|
|
|
|
|
|
|
|
|
|
2021-10-02 19:09:00 +08:00
|
|
|
|
def _wrap_exceptions():
|
2021-09-22 14:49:29 +08:00
|
|
|
|
def wrap_function(func):
|
|
|
|
|
@wraps(func)
|
|
|
|
|
def wrapped_func(*args, **kwargs):
|
|
|
|
|
try:
|
|
|
|
|
return func(*args, **kwargs)
|
|
|
|
|
except _mgp.UnknownError as e:
|
|
|
|
|
raise UnknownError(e)
|
|
|
|
|
except _mgp.UnableToAllocateError as e:
|
|
|
|
|
raise UnableToAllocateError(e)
|
|
|
|
|
except _mgp.InsufficientBufferError as e:
|
|
|
|
|
raise InsufficientBufferError(e)
|
|
|
|
|
except _mgp.OutOfRangeError as e:
|
|
|
|
|
raise OutOfRangeError(e)
|
|
|
|
|
except _mgp.LogicErrorError as e:
|
|
|
|
|
raise LogicErrorError(e)
|
|
|
|
|
except _mgp.DeletedObjectError as e:
|
|
|
|
|
raise DeletedObjectError(e)
|
|
|
|
|
except _mgp.InvalidArgumentError as e:
|
|
|
|
|
raise InvalidArgumentError(e)
|
|
|
|
|
except _mgp.KeyAlreadyExistsError as e:
|
|
|
|
|
raise KeyAlreadyExistsError(e)
|
|
|
|
|
except _mgp.ImmutableObjectError as e:
|
|
|
|
|
raise ImmutableObjectError(e)
|
|
|
|
|
except _mgp.ValueConversionError as e:
|
|
|
|
|
raise ValueConversionError(e)
|
|
|
|
|
except _mgp.SerializationError as e:
|
|
|
|
|
raise SerializationError(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)
|
2021-10-02 19:09:00 +08:00
|
|
|
|
elif inspect.isfunction(obj) and not name.startswith("_"):
|
2021-09-22 14:49:29 +08:00
|
|
|
|
setattr(module, name, wrap_function(obj))
|
|
|
|
|
|
|
|
|
|
|
2021-10-02 19:09:00 +08:00
|
|
|
|
_wrap_exceptions()
|