2022-07-07 21:05:56 +08:00
|
|
|
|
# Copyright 2022 Memgraph Ltd.
|
2021-10-03 18:07:04 +08:00
|
|
|
|
#
|
|
|
|
|
# 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+
|
|
|
|
|
|
2022-07-12 16:54:23 +08:00
|
|
|
|
import datetime
|
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
|
2022-07-12 16:54:23 +08:00
|
|
|
|
from collections import namedtuple
|
|
|
|
|
from functools import wraps
|
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.
|
|
|
|
|
"""
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UnknownError(_mgp.UnknownError):
|
|
|
|
|
"""
|
|
|
|
|
Signals unspecified failure.
|
|
|
|
|
"""
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UnableToAllocateError(_mgp.UnableToAllocateError):
|
|
|
|
|
"""
|
|
|
|
|
Signals failed memory allocation.
|
|
|
|
|
"""
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InsufficientBufferError(_mgp.InsufficientBufferError):
|
|
|
|
|
"""
|
|
|
|
|
Signals that some buffer is not big enough.
|
|
|
|
|
"""
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OutOfRangeError(_mgp.OutOfRangeError):
|
|
|
|
|
"""
|
|
|
|
|
Signals that an index-like parameter has a value that is outside its
|
|
|
|
|
possible values.
|
|
|
|
|
"""
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LogicErrorError(_mgp.LogicErrorError):
|
|
|
|
|
"""
|
|
|
|
|
Signals faulty logic within the program such as violating logical
|
|
|
|
|
preconditions or class invariants and may be preventable.
|
|
|
|
|
"""
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DeletedObjectError(_mgp.DeletedObjectError):
|
|
|
|
|
"""
|
|
|
|
|
Signals accessing an already deleted object.
|
|
|
|
|
"""
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InvalidArgumentError(_mgp.InvalidArgumentError):
|
|
|
|
|
"""
|
|
|
|
|
Signals that some of the arguments have invalid values.
|
|
|
|
|
"""
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class KeyAlreadyExistsError(_mgp.KeyAlreadyExistsError):
|
|
|
|
|
"""
|
|
|
|
|
Signals that a key already exists in a container-like object.
|
|
|
|
|
"""
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ImmutableObjectError(_mgp.ImmutableObjectError):
|
|
|
|
|
"""
|
|
|
|
|
Signals modification of an immutable object.
|
|
|
|
|
"""
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ValueConversionError(_mgp.ValueConversionError):
|
|
|
|
|
"""
|
|
|
|
|
Signals that the conversion failed between python and cypher values.
|
|
|
|
|
"""
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SerializationError(_mgp.SerializationError):
|
|
|
|
|
"""
|
|
|
|
|
Signals serialization error caused by concurrent modifications from
|
|
|
|
|
different transactions.
|
|
|
|
|
"""
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
2020-03-03 16:51:15 +08:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
class AuthorizationError(_mgp.AuthorizationError):
|
|
|
|
|
"""
|
|
|
|
|
Signals that the user doesn't have sufficient permissions to perform
|
|
|
|
|
procedure call.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2020-02-04 16:29:38 +08:00
|
|
|
|
class Label:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
"""Label of a `Vertex`."""
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
|
|
|
|
__slots__ = ("_name",)
|
2020-02-26 20:13:36 +08:00
|
|
|
|
|
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:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
"""
|
|
|
|
|
Get the name of the label.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Returns:
|
|
|
|
|
A string that represents the name of the label.
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
```label.name```
|
|
|
|
|
"""
|
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.
|
2022-04-21 21:45:31 +08:00
|
|
|
|
Property = namedtuple("Property", ("name", "value"))
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Properties:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
2022-07-07 21:05:56 +08:00
|
|
|
|
A collection of properties either on a `Vertex` or an `Edge`.
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
2022-04-21 21:45:31 +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)):
|
2022-04-21 21:45:31 +08:00
|
|
|
|
raise TypeError("Expected '_mgp.Vertex' or '_mgp.Edge', got {}".format(type(vertex_or_edge)))
|
2020-03-10 21:12:57 +08:00
|
|
|
|
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
|
|
|
|
"""
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Get the value of a property with the given name or return default value.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Args:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
property_name: String that represents property name.
|
|
|
|
|
default: Default value return if there is no property.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Any object value that property under `property_name` has or default value otherwise.
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
InvalidContextError: If `edge` or `vertex` is out of context.
|
2022-07-07 21:05:56 +08:00
|
|
|
|
UnableToAllocateError: If unable to allocate a `mgp.Value`.
|
|
|
|
|
DeletedObjectError: If the `object` has been deleted.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```
|
|
|
|
|
vertex.properties.get(property_name)
|
|
|
|
|
edge.properties.get(property_name)
|
|
|
|
|
```
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
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.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Args:
|
|
|
|
|
property_name: String that represents property name.
|
2022-07-07 21:05:56 +08:00
|
|
|
|
value: Object that represents value to be set.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Raises:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
UnableToAllocateError: If unable to allocate memory for storing the property.
|
|
|
|
|
ImmutableObjectError: If the object is immutable.
|
|
|
|
|
DeletedObjectError: If the object has been deleted.
|
|
|
|
|
SerializationError: If the object has been modified by another transaction.
|
|
|
|
|
ValueConversionError: If `value` is vertex, edge or path.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Examples:
|
|
|
|
|
```
|
|
|
|
|
vertex.properties.set(property_name, value)
|
|
|
|
|
edge.properties.set(property_name, value)
|
|
|
|
|
```
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
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
|
|
|
|
"""
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Iterate over the properties. Doesn’t return a dynamic view of the properties but copies the
|
2021-09-30 21:11:51 +08:00
|
|
|
|
current properties.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Returns:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Iterable `Property` of names and values.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Raises:
|
|
|
|
|
InvalidContextError: If edge or vertex is out of context.
|
2022-07-07 21:05:56 +08:00
|
|
|
|
UnableToAllocateError: If unable to allocate an iterator.
|
|
|
|
|
DeletedObjectError: If the object has been deleted.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```
|
|
|
|
|
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
|
|
|
|
|
```
|
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
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
|
|
|
|
"""
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Iterate over property names. Doesn’t return a dynamic view of the property names but copies the
|
2021-09-30 21:11:51 +08:00
|
|
|
|
name of the current properties.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Returns:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Iterable list of strings that represent names/keys of properties.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Raises:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
InvalidContextError: If edge or vertex is out of context.
|
|
|
|
|
UnableToAllocateError: If unable to allocate an iterator.
|
|
|
|
|
DeletedObjectError: If the object has been deleted.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```
|
|
|
|
|
graph.vertex.properties.keys()
|
|
|
|
|
graph.edge.properties.keys()
|
|
|
|
|
```
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
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
|
|
|
|
"""
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Iterate over property values. Doesn’t return a dynamic view of the property values but copies the
|
2021-09-30 21:11:51 +08:00
|
|
|
|
value of the current properties.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Returns:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Iterable list of property values.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Raises:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
InvalidContextError: If edge or vertex is out of context.
|
|
|
|
|
UnableToAllocateError: If unable to allocate an iterator.
|
|
|
|
|
DeletedObjectError: If the object has been deleted.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```
|
|
|
|
|
vertex.properties.values()
|
|
|
|
|
edge.properties.values()
|
|
|
|
|
```
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
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.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Returns:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
A number of properties on vertex or edge.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Raises:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
InvalidContextError: If edge or vertex is out of context.
|
|
|
|
|
UnableToAllocateError: If unable to allocate an iterator.
|
|
|
|
|
DeletedObjectError: If the object has been deleted.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```
|
|
|
|
|
len(vertex.properties)
|
|
|
|
|
len(edge.properties)
|
|
|
|
|
```
|
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
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
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Returns:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Iterable list of strings that represent names of properties.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Raises:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
InvalidContextError: If edge or vertex is out of context.
|
|
|
|
|
UnableToAllocateError: If unable to allocate an iterator.
|
|
|
|
|
DeletedObjectError: If the object has been deleted.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```
|
|
|
|
|
iter(vertex.properties)
|
|
|
|
|
iter(edge.properties)
|
|
|
|
|
```
|
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
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.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Args:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
property_name: String that represents property name.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Returns:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Any value that property under property_name have.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Raises:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
InvalidContextError: If edge or vertex is out of context.
|
|
|
|
|
UnableToAllocateError: If unable to allocate a mgp.Value.
|
|
|
|
|
DeletedObjectError: If the object has been deleted.
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```
|
|
|
|
|
vertex.properties[property_name]
|
|
|
|
|
edge.properties[property_name]
|
|
|
|
|
```
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
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.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Args:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
property_name: String that represents property name.
|
|
|
|
|
value: Object that represents value to be set.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Raises:
|
|
|
|
|
UnableToAllocateError: If unable to allocate memory for storing the property.
|
|
|
|
|
ImmutableObjectError: If the object is immutable.
|
|
|
|
|
DeletedObjectError: If the object has been deleted.
|
|
|
|
|
SerializationError: If the object has been modified by another transaction.
|
|
|
|
|
ValueConversionError: If `value` is vertex, edge or path.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```
|
|
|
|
|
vertex.properties[property_name] = value
|
|
|
|
|
edge.properties[property_name] = value
|
|
|
|
|
```
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
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.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Args:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
property_name: String that represents property name
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Returns:
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Bool value that depends if there is with a given name.
|
|
|
|
|
|
|
|
|
|
Raises:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
InvalidContextError: If edge or vertex is out of context.
|
|
|
|
|
UnableToAllocateError: If unable to allocate a mgp.Value.
|
|
|
|
|
DeletedObjectError: If the object has been deleted.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```
|
|
|
|
|
if property_name in vertex.properties:
|
|
|
|
|
```
|
|
|
|
|
```
|
|
|
|
|
if property_name in edge.properties:
|
|
|
|
|
```
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
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
|
|
|
|
|
2023-09-04 22:17:43 +08:00
|
|
|
|
def set_properties(self, properties: dict) -> None:
|
|
|
|
|
if not self._vertex_or_edge.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
|
|
|
|
|
self._vertex_or_edge.set_properties(properties)
|
|
|
|
|
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
class EdgeType:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Type of an Edge."""
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
|
|
|
|
__slots__ = ("_name",)
|
2020-02-25 17:24:16 +08:00
|
|
|
|
|
|
|
|
|
def __init__(self, name):
|
|
|
|
|
self._name = name
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def name(self) -> str:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
"""
|
|
|
|
|
Get the name of EdgeType.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Returns:
|
|
|
|
|
A string that represents the name of EdgeType.
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
```edge.type.name```
|
|
|
|
|
"""
|
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):
|
2022-04-21 21:45:31 +08:00
|
|
|
|
EdgeId = typing.NewType("EdgeId", int)
|
2020-03-18 21:07:56 +08:00
|
|
|
|
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.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
|
|
|
|
__slots__ = ("_edge",)
|
2020-02-25 17:24:16 +08:00
|
|
|
|
|
|
|
|
|
def __init__(self, edge):
|
|
|
|
|
if not isinstance(edge, _mgp.Edge):
|
2022-04-21 21:45:31 +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:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
"""
|
|
|
|
|
Check if `edge` is in a valid context and may be used.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Returns:
|
2022-09-08 23:48:34 +08:00
|
|
|
|
A `bool` value depends on if the `edge` is in a valid context.
|
|
|
|
|
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Examples:
|
|
|
|
|
```edge.is_valid()```
|
|
|
|
|
|
|
|
|
|
"""
|
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:
|
2022-09-08 23:48:34 +08:00
|
|
|
|
"""
|
|
|
|
|
Check if the `graph` can be modified.
|
2022-07-07 21:05:56 +08:00
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Returns:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
A `bool` value depends on if the `graph` is mutable.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```edge.underlying_graph_is_mutable()```
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
2022-07-07 21:05:56 +08:00
|
|
|
|
"""
|
2021-09-22 14:49:29 +08:00
|
|
|
|
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.
|
|
|
|
|
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Returns:
|
|
|
|
|
`EdgeId` represents ID of the edge.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Raises:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
InvalidContextError: If edge is out of context.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Examples:
|
|
|
|
|
```edge.id```
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
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
|
|
|
|
"""
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Get the type of edge.
|
|
|
|
|
|
|
|
|
|
Returns:
|
2022-09-08 23:48:34 +08:00
|
|
|
|
`EdgeType` describing the type of edge.
|
2021-09-22 14:49:29 +08:00
|
|
|
|
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Raises:
|
|
|
|
|
InvalidContextError: If edge is out of context.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```edge.type```
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
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
|
2022-04-21 21:45:31 +08:00
|
|
|
|
def from_vertex(self) -> "Vertex":
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Get the source vertex.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Returns:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
`Vertex` from where the edge is directed.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Raises:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
InvalidContextError: If edge is out of context.
|
|
|
|
|
|
|
|
|
|
Examples:
|
|
|
|
|
```edge.from_vertex```
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
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
|
2022-04-21 21:45:31 +08:00
|
|
|
|
def to_vertex(self) -> "Vertex":
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Get the destination vertex.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Returns:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
`Vertex` to where the edge is directed.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Raises:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
InvalidContextError: If edge is out of context.
|
2021-09-22 14:49:29 +08:00
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```edge.to_vertex```
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
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.
|
|
|
|
|
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Returns:
|
2022-09-08 23:48:34 +08:00
|
|
|
|
All `Properties` of edge.
|
2022-07-07 21:05:56 +08:00
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
InvalidContextError: If edge is out of context.
|
|
|
|
|
|
|
|
|
|
Examples:
|
|
|
|
|
```edge.properties```
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
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):
|
2022-04-21 21:45:31 +08:00
|
|
|
|
VertexId = typing.NewType("VertexId", int)
|
2020-02-27 19:24:26 +08:00
|
|
|
|
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
|
|
|
|
"""
|
2022-04-21 21:45:31 +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):
|
2022-04-21 21:45:31 +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:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
"""
|
|
|
|
|
Checks if `Vertex` is in valid context and may be used.
|
|
|
|
|
|
|
|
|
|
Returns:
|
2022-09-08 23:48:34 +08:00
|
|
|
|
A `bool` value depends on if the `Vertex` is in a valid context.
|
|
|
|
|
|
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```vertex.is_valid()```
|
|
|
|
|
|
|
|
|
|
"""
|
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:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
"""
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Check if the `graph` is mutable.
|
2022-07-07 21:05:56 +08:00
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Returns:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
A `bool` value depends on if the `graph` is mutable.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```vertex.underlying_graph_is_mutable()```
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
2022-07-07 21:05:56 +08:00
|
|
|
|
"""
|
2021-09-22 14:49:29 +08:00
|
|
|
|
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
|
|
|
|
"""
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Get the ID of the Vertex.
|
2021-09-22 14:49:29 +08:00
|
|
|
|
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Returns:
|
|
|
|
|
`VertexId` represents ID of the vertex.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Raises:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
InvalidContextError: If vertex is out of context.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Examples:
|
|
|
|
|
```vertex.id```
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
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.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Returns:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
A tuple of `Label` representing vertex Labels
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Raises:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
InvalidContextError: If vertex is out of context.
|
|
|
|
|
OutOfRangeError: If some of the labels are removed while collecting the labels.
|
|
|
|
|
DeletedObjectError: If `Vertex` has been deleted.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```vertex.labels```
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
2020-02-26 20:13:36 +08:00
|
|
|
|
if not self.is_valid():
|
2020-03-03 16:51:15 +08:00
|
|
|
|
raise InvalidContextError()
|
2022-04-21 21:45:31 +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.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Args:
|
|
|
|
|
label: String label to be added.
|
|
|
|
|
|
|
|
|
|
Raises:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
InvalidContextError: If `Vertex` is out of context.
|
|
|
|
|
UnableToAllocateError: If unable to allocate memory for storing the label.
|
|
|
|
|
ImmutableObjectError: If `Vertex` is immutable.
|
|
|
|
|
DeletedObjectError: If `Vertex` has been deleted.
|
|
|
|
|
SerializationError: If `Vertex` has been modified by another transaction.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```vertex.add_label(label)```
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
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.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Args:
|
|
|
|
|
label: String label to be deleted
|
|
|
|
|
Raises:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
InvalidContextError: If `Vertex` is out of context.
|
|
|
|
|
ImmutableObjectError: If `Vertex` is immutable.
|
|
|
|
|
DeletedObjectError: If `Vertex` has been deleted.
|
|
|
|
|
SerializationError: If `Vertex` has been modified by another transaction.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```vertex.remove_label(label)```
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
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.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Returns:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
`Properties` on a current vertex.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Raises:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
InvalidContextError: If `Vertex` is out of context.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```vertex.properties```
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
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.
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Doesn’t return a dynamic view of the edges but copies the
|
2021-09-30 21:11:51 +08:00
|
|
|
|
current inbound edges.
|
|
|
|
|
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Returns:
|
|
|
|
|
Iterable list of `Edge` objects that are directed in towards the current vertex.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Raises:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
InvalidContextError: If `Vertex` is out of context.
|
|
|
|
|
UnableToAllocateError: If unable to allocate an iterator.
|
|
|
|
|
DeletedObjectError: If `Vertex` has been deleted.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```for edge in vertex.in_edges:```
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
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.
|
|
|
|
|
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Doesn’t return a dynamic view of the edges but copies the
|
2021-09-30 21:11:51 +08:00
|
|
|
|
current outbound edges.
|
|
|
|
|
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Returns:
|
|
|
|
|
Iterable list of `Edge` objects that are directed out of the current vertex.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Raises:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
InvalidContextError: If `Vertex` is out of context.
|
|
|
|
|
UnableToAllocateError: If unable to allocate an iterator.
|
|
|
|
|
DeletedObjectError: If `Vertex` has been deleted.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```for edge in vertex.out_edges:```
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
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."""
|
2022-04-21 21:45:31 +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
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Raises:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
InvalidContextError: If passed in Vertex is invalid.
|
|
|
|
|
UnableToAllocateError: If cannot allocate a path.
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
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:
|
2022-04-21 21:45:31 +08:00
|
|
|
|
raise TypeError("Expected '_mgp.Vertex' or '_mgp.Path', got '{}'".format(type(starting_vertex_or_path)))
|
2020-02-28 16:44:39 +08:00
|
|
|
|
|
|
|
|
|
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:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
"""
|
|
|
|
|
Check if `Path` is in valid context and may be used.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Returns:
|
2022-09-08 23:48:34 +08:00
|
|
|
|
A `bool` value depends on if the `Path` is in a valid context.
|
|
|
|
|
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Examples:
|
|
|
|
|
```path.is_valid()```
|
|
|
|
|
"""
|
2020-02-28 16:44:39 +08:00
|
|
|
|
return self._path.is_valid()
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
def expand(self, edge: Edge):
|
2022-07-07 21:05:56 +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.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Args:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
edge: `Edge` that is added to the path
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Raises:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
InvalidContextError: If using an invalid `Path` instance or if passed in `Edge` is invalid.
|
|
|
|
|
LogicErrorError: If the current last vertex in the path is not part of the given edge.
|
|
|
|
|
UnableToAllocateError: If unable to allocate memory for path extension.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```path.expand(edge)```
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
2020-02-28 16:44:39 +08:00
|
|
|
|
if not isinstance(edge, Edge):
|
2022-04-21 21:45:31 +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
|
|
|
|
|
2023-09-19 18:37:55 +08:00
|
|
|
|
def pop(self):
|
|
|
|
|
"""
|
|
|
|
|
Remove the last node and the last relationship from the path.
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
InvalidContextError: If using an invalid `Path` instance
|
|
|
|
|
OutOfRangeError: If the path contains no relationships.
|
|
|
|
|
|
|
|
|
|
Examples:
|
|
|
|
|
```path.pop()```
|
|
|
|
|
"""
|
|
|
|
|
if not self.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
self._path.pop()
|
|
|
|
|
# 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
|
|
|
|
"""
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Vertices are ordered from the start to the end of the path.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Returns:
|
|
|
|
|
A tuple of `Vertex` objects order from start to end of the path.
|
2022-07-07 21:05:56 +08:00
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
InvalidContextError: If using an invalid Path instance.
|
2020-02-28 16:44:39 +08:00
|
|
|
|
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Examples:
|
2022-09-08 23:48:34 +08:00
|
|
|
|
```path.vertices```
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
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
|
2022-04-21 21:45:31 +08:00
|
|
|
|
self._vertices = tuple(Vertex(self._path.vertex_at(i)) for i in range(num_vertices))
|
2020-02-28 16:44:39 +08:00
|
|
|
|
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
|
|
|
|
"""
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Edges are ordered from the start to the end of the path.
|
2020-02-28 16:44:39 +08:00
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Returns:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
A tuple of `Edge` objects order from start to end of the path
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Raises:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
InvalidContextError: If using an invalid `Path` instance.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```path.edges```
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
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()
|
2022-04-21 21:45:31 +08:00
|
|
|
|
self._edges = tuple(Edge(self._path.edge_at(i)) for i in range(num_edges))
|
2020-02-28 16:44:39 +08:00
|
|
|
|
return self._edges
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
2023-09-19 18:37:55 +08:00
|
|
|
|
@property
|
|
|
|
|
def length(self) -> int:
|
|
|
|
|
return self._path.size()
|
|
|
|
|
|
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."""
|
2022-04-21 21:45:31 +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."""
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
|
|
|
|
__slots__ = ("_graph", "_len")
|
2020-02-12 17:27:59 +08:00
|
|
|
|
|
|
|
|
|
def __init__(self, graph):
|
|
|
|
|
if not isinstance(graph, _mgp.Graph):
|
2022-04-21 21:45:31 +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:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
"""
|
|
|
|
|
Check if `Vertices` is in valid context and may be used.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Returns:
|
2022-09-08 23:48:34 +08:00
|
|
|
|
A `bool` value depends on if the `Vertices` is in valid context.
|
|
|
|
|
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Examples:
|
|
|
|
|
```vertices.is_valid()```
|
|
|
|
|
"""
|
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.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Returns:
|
|
|
|
|
Iterable list of `Vertex` objects.
|
2022-07-07 21:05:56 +08:00
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Raises:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
InvalidContextError: If context is invalid.
|
|
|
|
|
UnableToAllocateError: If unable to allocate an iterator or a vertex.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```
|
|
|
|
|
for vertex in graph.vertices:
|
|
|
|
|
```
|
|
|
|
|
```
|
|
|
|
|
iter(graph.vertices)
|
|
|
|
|
```
|
|
|
|
|
|
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
|
|
|
|
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
|
|
|
|
"""
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Check if Vertices contain the given vertex.
|
2022-07-07 21:05:56 +08:00
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Args:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
vertex: `Vertex` to be checked if it is a part of graph `Vertices`.
|
|
|
|
|
|
|
|
|
|
Returns:
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Bool value depends if there is `Vertex` in graph `Vertices`.
|
2022-07-07 21:05:56 +08:00
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
UnableToAllocateError: If unable to allocate the vertex.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```if vertex in graph.vertices:```
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
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.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Returns:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
A number of vertices in the graph.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Raises:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
InvalidContextError: If context is invalid.
|
|
|
|
|
UnableToAllocateError: If unable to allocate an iterator or a vertex.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```len(graph.vertices)```
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
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."""
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
|
|
|
|
__slots__ = ("_graph",)
|
2020-02-12 17:27:59 +08:00
|
|
|
|
|
|
|
|
|
def __init__(self, graph):
|
|
|
|
|
if not isinstance(graph, _mgp.Graph):
|
2022-04-21 21:45:31 +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:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
"""
|
|
|
|
|
Check if `graph` is in a valid context and may be used.
|
|
|
|
|
|
|
|
|
|
Returns:
|
2022-09-08 23:48:34 +08:00
|
|
|
|
A `bool` value depends on if the `graph` is in a valid context.
|
|
|
|
|
|
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```graph.is_valid()```
|
|
|
|
|
|
|
|
|
|
"""
|
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
|
|
|
|
"""
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Return the Vertex corresponding to the 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.
|
|
|
|
|
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Args:
|
|
|
|
|
vertex_id: Memgraph Vertex ID
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
`Vertex`corresponding to `vertex_id`
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
IndexError: If unable to find the given vertex_id.
|
|
|
|
|
InvalidContextError: If context is invalid.
|
|
|
|
|
|
|
|
|
|
Examples:
|
|
|
|
|
```graph.get_vertex_by_id(vertex_id)```
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
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
|
|
|
|
"""
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Get 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.
|
|
|
|
|
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Returns:
|
|
|
|
|
`Vertices` that contained in the graph.
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
InvalidContextError: If context is invalid.
|
|
|
|
|
|
|
|
|
|
Examples:
|
|
|
|
|
Iteration over all graph `Vertices`.
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
graph = context.graph
|
|
|
|
|
for vertex in graph.vertices:
|
|
|
|
|
```
|
|
|
|
|
|
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:
|
|
|
|
|
"""
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Check if the graph is mutable. Thus it can be used to modify vertices and edges.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Returns:
|
|
|
|
|
A `bool` value that depends if the graph is mutable or not.
|
|
|
|
|
|
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```graph.is_mutable()```
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
if not self.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
return self._graph.is_mutable()
|
|
|
|
|
|
|
|
|
|
def create_vertex(self) -> Vertex:
|
|
|
|
|
"""
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Create an empty vertex.
|
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Returns:
|
|
|
|
|
Created `Vertex`.
|
2022-07-07 21:05:56 +08:00
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Raises:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
ImmutableObjectError: If `graph` is immutable.
|
|
|
|
|
UnableToAllocateError: If unable to allocate a vertex.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Creating an empty vertex.
|
|
|
|
|
```vertex = graph.create_vertex()```
|
2021-09-22 14:49:29 +08:00
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
if not self.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
return Vertex(self._graph.create_vertex())
|
|
|
|
|
|
|
|
|
|
def delete_vertex(self, vertex: Vertex) -> None:
|
|
|
|
|
"""
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Delete a vertex if there are no edges.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
vertex: `Vertex` to be deleted
|
|
|
|
|
Raises:
|
|
|
|
|
ImmutableObjectError: If `graph` is immutable.
|
|
|
|
|
LogicErrorError: If `vertex` has edges.
|
|
|
|
|
SerializationError: If `vertex` has been modified by
|
|
|
|
|
another transaction.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```graph.delete_vertex(vertex)```
|
2021-09-22 14:49:29 +08:00
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
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.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Args:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
vertex: `Vertex` to be deleted with all of its edges
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Raises:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
ImmutableObjectError: If `graph` is immutable.
|
|
|
|
|
SerializationError: If `vertex` has been modified by another transaction.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```graph.detach_delete_vertex(vertex)```
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
if not self.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
|
|
|
|
self._graph.detach_delete_vertex(vertex._vertex)
|
|
|
|
|
|
2022-12-23 17:08:52 +08:00
|
|
|
|
def create_edge(self, from_vertex: Vertex, to_vertex: Vertex, edge_type: EdgeType) -> Edge:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
Create an edge.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
from_vertex: `Vertex` from where edge is directed.
|
|
|
|
|
to_vertex: `Vertex' to where edge is directed.
|
|
|
|
|
edge_type: `EdgeType` defines the type of edge.
|
2022-07-07 21:05:56 +08:00
|
|
|
|
|
2022-12-23 17:08:52 +08:00
|
|
|
|
Returns:
|
|
|
|
|
Created `Edge`.
|
|
|
|
|
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Raises:
|
|
|
|
|
ImmutableObjectError: If `graph` is immutable.
|
|
|
|
|
UnableToAllocateError: If unable to allocate an edge.
|
|
|
|
|
DeletedObjectError: If `from_vertex` or `to_vertex` has been deleted.
|
|
|
|
|
SerializationError: If `from_vertex` or `to_vertex` has been modified by another transaction.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Examples:
|
2022-12-23 17:08:52 +08:00
|
|
|
|
```edge = graph.create_edge(from_vertex, vertex, edge_type)```
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
if not self.is_valid():
|
|
|
|
|
raise InvalidContextError()
|
2022-04-21 21:45:31 +08:00
|
|
|
|
return Edge(self._graph.create_edge(from_vertex._vertex, to_vertex._vertex, edge_type.name))
|
2021-09-22 14:49:29 +08:00
|
|
|
|
|
|
|
|
|
def delete_edge(self, edge: Edge) -> None:
|
|
|
|
|
"""
|
|
|
|
|
Delete an edge.
|
|
|
|
|
|
2022-07-07 21:05:56 +08:00
|
|
|
|
Args:
|
|
|
|
|
edge: `Edge` to be deleted
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Raises:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
ImmutableObjectError if `graph` is immutable.
|
|
|
|
|
Raise SerializationError if `edge`, its source or destination vertex has been modified by another transaction.
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""
|
|
|
|
|
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."""
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
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
|
|
|
|
"""
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
|
|
|
|
__slots__ = ("_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
|
|
|
|
|
|
|
|
|
def __init__(self, graph):
|
|
|
|
|
if not isinstance(graph, _mgp.Graph):
|
2022-04-21 21:45:31 +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:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
"""
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Access to `Graph` object.
|
2022-07-07 21:05:56 +08:00
|
|
|
|
|
2022-09-08 23:48:34 +08:00
|
|
|
|
Returns:
|
|
|
|
|
Graph object.
|
|
|
|
|
|
|
|
|
|
Raises:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
InvalidContextError: If context is invalid.
|
2022-09-08 23:48:34 +08:00
|
|
|
|
|
|
|
|
|
Examples:
|
2022-07-07 21:05:56 +08:00
|
|
|
|
```context.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
|
|
|
|
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
|
|
|
|
|
|
2022-04-21 21:45:31 +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_))
|
|
|
|
|
|
|
|
|
|
|
2023-06-26 21:46:13 +08:00
|
|
|
|
class UnequalTypesError(Exception):
|
|
|
|
|
"""Signals a typing annotation is not equal between types"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, type1_: typing.Any, type2_: typing.Any):
|
|
|
|
|
super().__init__(f"Unequal typing annotation '{type1_}' and '{type2_}'")
|
|
|
|
|
|
|
|
|
|
|
2020-02-27 19:24:26 +08:00
|
|
|
|
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(),
|
2022-04-21 21:45:31 +08:00
|
|
|
|
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:
|
2022-04-21 21:45:31 +08:00
|
|
|
|
(type_arg,) = types
|
2020-02-27 19:24:26 +08:00
|
|
|
|
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:
|
2022-04-21 21:45:31 +08:00
|
|
|
|
(type_arg,) = type_args
|
2020-02-27 19:24:26 +08:00
|
|
|
|
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):
|
2022-04-21 21:45:31 +08:00
|
|
|
|
return tuple(
|
|
|
|
|
map(
|
|
|
|
|
str.strip,
|
|
|
|
|
type_as_str[type_as_str.index("[") + 1 : -1].split(","),
|
|
|
|
|
)
|
|
|
|
|
)
|
2020-02-27 19:24:26 +08:00
|
|
|
|
|
2020-03-12 20:51:59 +08:00
|
|
|
|
def fully_qualified_name(cls):
|
2022-04-21 21:45:31 +08:00
|
|
|
|
if cls.__module__ is None or cls.__module__ == "builtins":
|
2020-03-12 20:51:59 +08:00
|
|
|
|
return cls.__name__
|
2022-04-21 21:45:31 +08:00
|
|
|
|
return cls.__module__ + "." + cls.__name__
|
2020-03-12 20:51:59 +08:00
|
|
|
|
|
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):
|
2022-04-21 21:45:31 +08:00
|
|
|
|
if type_as_str.startswith("typing.Union"):
|
2020-02-27 19:24:26 +08:00
|
|
|
|
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:
|
2022-04-21 21:45:31 +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:
|
2022-04-21 21:45:31 +08:00
|
|
|
|
(type_arg_as_str,) = types
|
2020-02-27 19:24:26 +08:00
|
|
|
|
else:
|
2022-04-21 21:45:31 +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))
|
2022-04-21 21:45:31 +08:00
|
|
|
|
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.
|
2022-04-21 21:45:31 +08:00
|
|
|
|
type_arg_as_str = ", ".join(type_arg_as_str)
|
2021-04-27 14:31:18 +08:00
|
|
|
|
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_))
|
|
|
|
|
|
|
|
|
|
|
2023-06-26 21:46:13 +08:00
|
|
|
|
def _is_typing_same(type1_, type2_):
|
|
|
|
|
"""Convert typing annotation to a _mgp.CypherType instance."""
|
|
|
|
|
simple_types = {
|
|
|
|
|
typing.Any: 1,
|
|
|
|
|
object: 2,
|
|
|
|
|
list: 3,
|
|
|
|
|
Any: 4,
|
|
|
|
|
bool: 5,
|
|
|
|
|
str: 6,
|
|
|
|
|
int: 7,
|
|
|
|
|
float: 8,
|
|
|
|
|
Number: 9,
|
|
|
|
|
Map: 10,
|
|
|
|
|
Vertex: 11,
|
|
|
|
|
Edge: 12,
|
|
|
|
|
Path: 13,
|
|
|
|
|
Date: 14,
|
|
|
|
|
LocalTime: 15,
|
|
|
|
|
LocalDateTime: 16,
|
|
|
|
|
Duration: 17,
|
|
|
|
|
}
|
|
|
|
|
try:
|
|
|
|
|
return simple_types[type1_] == simple_types[type2_]
|
|
|
|
|
except KeyError:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
if sys.version_info < (3, 8):
|
|
|
|
|
# skip type checks
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
complex_type1 = typing.get_origin(type1_)
|
|
|
|
|
type_args1 = typing.get_args(type2_)
|
|
|
|
|
|
|
|
|
|
complex_type2 = typing.get_origin(type1_)
|
|
|
|
|
type_args2 = typing.get_args(type2_)
|
|
|
|
|
|
|
|
|
|
if complex_type2 != complex_type1:
|
|
|
|
|
raise UnequalTypesError(type1_, type2_)
|
|
|
|
|
|
|
|
|
|
if complex_type1 == typing.Union:
|
|
|
|
|
contains_none_arg1 = type(None) in type_args1
|
|
|
|
|
contains_none_arg2 = type(None) in type_args2
|
|
|
|
|
|
|
|
|
|
if contains_none_arg1 != contains_none_arg2:
|
|
|
|
|
raise UnequalTypesError(type1_, type2_)
|
|
|
|
|
|
|
|
|
|
if contains_none_arg1:
|
|
|
|
|
types1 = tuple(t for t in type_args1 if t is not type(None)) # noqa E721
|
|
|
|
|
types2 = tuple(t for t in type_args2 if t is not type(None)) # noqa E721
|
|
|
|
|
if len(types1) != len(types2):
|
|
|
|
|
raise UnequalTypesError(types1, types2)
|
|
|
|
|
if len(types1) == 1:
|
|
|
|
|
(type_arg1,) = types1
|
|
|
|
|
(type_arg2,) = types2
|
|
|
|
|
else:
|
|
|
|
|
type_arg1 = typing.Union.__getitem__(types1)
|
|
|
|
|
type_arg2 = typing.Union.__getitem__(types2)
|
|
|
|
|
return _is_typing_same(type_arg1, type_arg2)
|
|
|
|
|
elif complex_type1 == list:
|
|
|
|
|
(type_arg1,) = type_args1
|
|
|
|
|
(type_arg2,) = type_args2
|
|
|
|
|
return _is_typing_same(type_arg1, type_arg2)
|
|
|
|
|
# skip type checks
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
2020-02-04 16:29:38 +08:00
|
|
|
|
# Procedure registration
|
|
|
|
|
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
2020-02-04 16:29:38 +08:00
|
|
|
|
class Deprecated:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Annotate a resulting Record's field as deprecated."""
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
|
|
|
|
__slots__ = ("field_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
|
|
|
|
|
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):
|
2022-04-21 21:45:31 +08:00
|
|
|
|
raise TypeError("Expected a callable object, got an instance of '{}'".format(type(func)))
|
2021-07-01 18:36:41 +08:00
|
|
|
|
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
|
|
|
|
|
2022-04-21 21:45:31 +08:00
|
|
|
|
def _register_proc(func: typing.Callable[..., Record], is_write: bool):
|
2021-09-22 14:49:29 +08:00
|
|
|
|
raise_if_does_not_meet_requirements(func)
|
2022-04-21 21:45:31 +08:00
|
|
|
|
register_func = _mgp.Module.add_write_procedure if is_write else _mgp.Module.add_read_procedure
|
2021-09-22 14:49:29 +08:00
|
|
|
|
sig = inspect.signature(func)
|
|
|
|
|
params = tuple(sig.parameters.values())
|
|
|
|
|
if params and params[0].annotation is ProcCtx:
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
@wraps(func)
|
|
|
|
|
def wrapper(graph, args):
|
|
|
|
|
return func(ProcCtx(graph), *args)
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
params = params[1:]
|
|
|
|
|
mgp_proc = register_func(_mgp._MODULE, wrapper)
|
|
|
|
|
else:
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
@wraps(func)
|
|
|
|
|
def wrapper(graph, args):
|
|
|
|
|
return func(*args)
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
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):
|
2022-04-21 21:45:31 +08:00
|
|
|
|
raise TypeError("Expected '{}' to return 'mgp.Record', got '{}'".format(func.__name__, type(record)))
|
2021-09-22 14:49:29 +08:00
|
|
|
|
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
|
|
|
|
|
2022-05-17 21:33:28 +08:00
|
|
|
|
The decorator `read_proc` is meant to be used to register module procedures.
|
|
|
|
|
The registered `func` needs to be a callable which optionally takes
|
|
|
|
|
`ProcCtx` as its 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.
|
2020-02-04 16:29:38 +08:00
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
2022-05-17 21:33:28 +08:00
|
|
|
|
The decorator `write_proc` is meant to be used to register module
|
2021-09-22 14:49:29 +08:00
|
|
|
|
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
|
2022-05-17 21:33:28 +08:00
|
|
|
|
`Record(field_name=Deprecated(type), ...)`. Multiple records can be produced
|
|
|
|
|
by returning an iterable of them. Registering generator functions is
|
|
|
|
|
currently not supported.
|
2021-09-22 14:49:29 +08:00
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2023-06-26 21:46:13 +08:00
|
|
|
|
def _register_batch_proc(
|
|
|
|
|
func: typing.Callable[..., Record], initializer: typing.Callable, cleanup: typing.Callable, is_write: bool
|
|
|
|
|
):
|
|
|
|
|
raise_if_does_not_meet_requirements(func)
|
|
|
|
|
register_func = _mgp.Module.add_batch_write_procedure if is_write else _mgp.Module.add_batch_read_procedure
|
|
|
|
|
func_sig = inspect.signature(func)
|
|
|
|
|
func_params = tuple(func_sig.parameters.values())
|
|
|
|
|
|
|
|
|
|
initializer_sig = inspect.signature(initializer)
|
|
|
|
|
initializer_params = tuple(initializer_sig.parameters.values())
|
|
|
|
|
|
|
|
|
|
assert (
|
|
|
|
|
func_params and initializer_params or not func_params and not initializer_params
|
|
|
|
|
), "Both function params and initializer params must exist or not exist"
|
|
|
|
|
|
|
|
|
|
assert len(func_params) == len(initializer_params), "Number of params must be same"
|
|
|
|
|
|
|
|
|
|
assert initializer_sig.return_annotation is initializer_sig.empty, "Initializer can't return anything"
|
|
|
|
|
|
|
|
|
|
if func_params and func_params[0].annotation is ProcCtx:
|
|
|
|
|
assert (
|
|
|
|
|
initializer_params and initializer_params[0].annotation is ProcCtx
|
|
|
|
|
), "Initializer must have mgp.ProcCtx as first parameter"
|
|
|
|
|
|
|
|
|
|
@wraps(func)
|
|
|
|
|
def wrapper_func(graph, args):
|
|
|
|
|
return func(ProcCtx(graph), *args)
|
|
|
|
|
|
|
|
|
|
@wraps(initializer)
|
|
|
|
|
def wrapper_initializer(graph, args):
|
|
|
|
|
return initializer(ProcCtx(graph), *args)
|
|
|
|
|
|
|
|
|
|
func_params = func_params[1:]
|
|
|
|
|
initializer_params = initializer_params[1:]
|
|
|
|
|
mgp_proc = register_func(_mgp._MODULE, wrapper_func, wrapper_initializer, cleanup)
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
|
|
@wraps(func)
|
|
|
|
|
def wrapper_func(graph, args):
|
|
|
|
|
return func(*args)
|
|
|
|
|
|
|
|
|
|
@wraps(initializer)
|
|
|
|
|
def wrapper_initializer(graph, args):
|
|
|
|
|
return initializer(*args)
|
|
|
|
|
|
|
|
|
|
mgp_proc = register_func(_mgp._MODULE, wrapper_func, wrapper_initializer, cleanup)
|
|
|
|
|
|
|
|
|
|
for func_param, initializer_param in zip(func_params, initializer_params):
|
|
|
|
|
func_param_name = func_param.name
|
|
|
|
|
func_param_type_ = func_param.annotation
|
|
|
|
|
if func_param_type_ is func_param.empty:
|
|
|
|
|
func_param_type_ = object
|
|
|
|
|
initializer_param_type_ = initializer_param.annotation
|
|
|
|
|
if initializer_param.annotation is initializer_param.empty:
|
|
|
|
|
initializer_param_type_ = object
|
|
|
|
|
|
|
|
|
|
assert _is_typing_same(
|
|
|
|
|
func_param_type_, initializer_param_type_
|
|
|
|
|
), "Types of initializer and function must be same"
|
|
|
|
|
|
|
|
|
|
func_cypher_type = _typing_to_cypher_type(func_param_type_)
|
|
|
|
|
if func_param.default is func_param.empty:
|
|
|
|
|
mgp_proc.add_arg(func_param_name, func_cypher_type)
|
|
|
|
|
else:
|
|
|
|
|
mgp_proc.add_opt_arg(func_param_name, func_cypher_type, func_param.default)
|
|
|
|
|
if func_sig.return_annotation is not func_sig.empty:
|
|
|
|
|
record = func_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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_batch_write_proc(func: typing.Callable[..., Record], initializer: typing.Callable, cleanup: typing.Callable):
|
|
|
|
|
return _register_batch_proc(func, initializer, cleanup, True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_batch_read_proc(func: typing.Callable[..., Record], initializer: typing.Callable, cleanup: typing.Callable):
|
|
|
|
|
return _register_batch_proc(func, initializer, cleanup, False)
|
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
"""
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
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
|
|
|
|
|
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
2021-07-05 17:42:40 +08:00
|
|
|
|
class Message:
|
2021-09-22 14:49:29 +08:00
|
|
|
|
"""Represents a message from a stream."""
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
|
|
|
|
__slots__ = ("_message",)
|
2021-07-01 18:36:41 +08:00
|
|
|
|
|
|
|
|
|
def __init__(self, message):
|
|
|
|
|
if not isinstance(message, _mgp.Message):
|
2022-04-21 21:45:31 +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:
|
2021-11-16 23:12:38 +08:00
|
|
|
|
"""
|
|
|
|
|
Supported stream sources:
|
|
|
|
|
- Kafka
|
|
|
|
|
|
|
|
|
|
Raise InvalidArgumentError if the message is from an unsupported stream source.
|
|
|
|
|
"""
|
2021-11-11 19:07:58 +08:00
|
|
|
|
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."""
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
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."""
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
|
|
|
|
__slots__ = ("_messages",)
|
2021-07-01 18:36:41 +08:00
|
|
|
|
|
|
|
|
|
def __init__(self, messages):
|
|
|
|
|
if not isinstance(messages, _mgp.Messages):
|
2022-04-21 21:45:31 +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
|
|
|
|
"""
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
|
|
|
|
__slots__ = "_graph"
|
2021-07-01 18:36:41 +08:00
|
|
|
|
|
|
|
|
|
def __init__(self, graph):
|
|
|
|
|
if not isinstance(graph, _mgp.Graph):
|
2022-04-21 21:45:31 +08:00
|
|
|
|
raise TypeError("Expected '_mgp.Graph', got '{}'".format(type(graph)))
|
2021-07-05 17:42:40 +08:00
|
|
|
|
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:
|
2022-04-21 21:45:31 +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:
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
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)
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
2021-07-01 18:36:41 +08:00
|
|
|
|
_mgp._MODULE.add_transformation(wrapper)
|
|
|
|
|
else:
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
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)
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
2021-07-01 18:36:41 +08:00
|
|
|
|
_mgp._MODULE.add_transformation(wrapper)
|
|
|
|
|
return func
|
2021-09-22 14:49:29 +08:00
|
|
|
|
|
|
|
|
|
|
2022-04-21 21:45:31 +08:00
|
|
|
|
class FuncCtx:
|
|
|
|
|
"""Context of a function being executed.
|
|
|
|
|
|
2022-05-17 21:33:28 +08:00
|
|
|
|
Access to a FuncCtx is only valid during a single execution of a function in
|
|
|
|
|
a query. You should not globally store a FuncCtx instance. The graph object
|
|
|
|
|
within the FuncCtx is not mutable.
|
2022-04-21 21:45:31 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
__slots__ = "_graph"
|
|
|
|
|
|
|
|
|
|
def __init__(self, graph):
|
|
|
|
|
if not isinstance(graph, _mgp.Graph):
|
|
|
|
|
raise TypeError("Expected '_mgp.Graph', got '{}'".format(type(graph)))
|
|
|
|
|
self._graph = Graph(graph)
|
|
|
|
|
|
|
|
|
|
def is_valid(self) -> bool:
|
|
|
|
|
return self._graph.is_valid()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def function(func: typing.Callable):
|
2022-05-17 21:33:28 +08:00
|
|
|
|
"""
|
|
|
|
|
Register `func` as a user-defined function in the current module.
|
|
|
|
|
|
|
|
|
|
The decorator `function` is meant to be used to register module functions.
|
|
|
|
|
The registered `func` needs to be a callable which optionally takes
|
|
|
|
|
`FuncCtx` as its first argument. Other arguments of `func` will be bound to
|
2022-07-07 21:05:56 +08:00
|
|
|
|
values passed in the Cypher query. Only the function arguments need to be
|
2022-05-17 21:33:28 +08:00
|
|
|
|
annotated with types. The return type doesn't need to be specified, but it
|
|
|
|
|
has to be supported by `mgp.Any`. Registering generator functions is
|
|
|
|
|
currently not supported.
|
|
|
|
|
|
|
|
|
|
Example usage.
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
import mgp
|
|
|
|
|
@mgp.function
|
|
|
|
|
def func_example(context: mgp.FuncCtx,
|
|
|
|
|
required_arg: str,
|
|
|
|
|
optional_arg: mgp.Nullable[str] = None
|
|
|
|
|
):
|
|
|
|
|
return_args = [required_arg]
|
|
|
|
|
if optional_arg is not None:
|
|
|
|
|
return_args.append(optional_arg)
|
|
|
|
|
# Return any kind of result supported by mgp.Any
|
|
|
|
|
return return_args
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The example function above returns a list of provided arguments:
|
|
|
|
|
* `required_arg` is always present and its value is the first argument of
|
|
|
|
|
the function.
|
|
|
|
|
* `optional_arg` is present if the second argument of the function is not
|
|
|
|
|
`null`.
|
|
|
|
|
Any errors can be reported by raising an Exception.
|
|
|
|
|
|
|
|
|
|
The function can be invoked in Cypher using the following calls:
|
|
|
|
|
RETURN example.func_example("first argument", "second_argument");
|
|
|
|
|
RETURN example.func_example("first argument");
|
|
|
|
|
Naturally, you may pass in different arguments.
|
|
|
|
|
"""
|
2022-04-21 21:45:31 +08:00
|
|
|
|
raise_if_does_not_meet_requirements(func)
|
|
|
|
|
register_func = _mgp.Module.add_function
|
|
|
|
|
sig = inspect.signature(func)
|
|
|
|
|
params = tuple(sig.parameters.values())
|
|
|
|
|
if params and params[0].annotation is FuncCtx:
|
|
|
|
|
|
|
|
|
|
@wraps(func)
|
|
|
|
|
def wrapper(graph, args):
|
|
|
|
|
return func(FuncCtx(graph), *args)
|
|
|
|
|
|
|
|
|
|
params = params[1:]
|
|
|
|
|
mgp_func = register_func(_mgp._MODULE, wrapper)
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
|
|
@wraps(func)
|
|
|
|
|
def wrapper(graph, args):
|
|
|
|
|
return func(*args)
|
|
|
|
|
|
|
|
|
|
mgp_func = 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_func.add_arg(name, cypher_type)
|
|
|
|
|
else:
|
|
|
|
|
mgp_func.add_opt_arg(name, cypher_type, param.default)
|
|
|
|
|
return func
|
|
|
|
|
|
|
|
|
|
|
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)
|
2022-09-08 23:48:34 +08:00
|
|
|
|
except _mgp.AuthorizationError as e:
|
|
|
|
|
raise AuthorizationError(e)
|
2022-04-21 21:45:31 +08:00
|
|
|
|
|
2021-09-22 14:49:29 +08:00
|
|
|
|
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):
|
2022-04-21 21:45:31 +08:00
|
|
|
|
setattr(
|
|
|
|
|
cls,
|
|
|
|
|
name,
|
|
|
|
|
property(
|
|
|
|
|
wrap_prop_func(obj.fget),
|
|
|
|
|
wrap_prop_func(obj.fset),
|
|
|
|
|
wrap_prop_func(obj.fdel),
|
|
|
|
|
obj.__doc__,
|
|
|
|
|
),
|
|
|
|
|
)
|
2021-09-22 14:49:29 +08:00
|
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
|
|
|
|
|
|
2022-08-22 19:47:52 +08:00
|
|
|
|
class Logger:
|
|
|
|
|
"""Represents a Logger through which it is possible
|
|
|
|
|
to send logs via API to the graph database.
|
|
|
|
|
|
|
|
|
|
The best way to use this Logger is to have one per query module."""
|
|
|
|
|
|
|
|
|
|
__slots__ = ("_logger",)
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self._logger = _mgp._LOGGER
|
|
|
|
|
|
|
|
|
|
def info(self, out: str) -> None:
|
|
|
|
|
"""
|
|
|
|
|
Log message on INFO level..
|
|
|
|
|
Args:
|
|
|
|
|
out: String message to be logged.
|
|
|
|
|
|
|
|
|
|
Examples:
|
|
|
|
|
```logger.info("Hello from query module.")```
|
|
|
|
|
"""
|
|
|
|
|
self._logger.info(out)
|
|
|
|
|
|
|
|
|
|
def warning(self, out: str) -> None:
|
|
|
|
|
"""
|
|
|
|
|
Log message on WARNING level..
|
|
|
|
|
Args:
|
|
|
|
|
out: String message to be logged.
|
|
|
|
|
|
|
|
|
|
Examples:
|
|
|
|
|
```logger.warning("Hello from query module.")```
|
|
|
|
|
"""
|
|
|
|
|
self._logger.warning(out)
|
|
|
|
|
|
|
|
|
|
def critical(self, out: str) -> None:
|
|
|
|
|
"""
|
|
|
|
|
Log message on CRITICAL level..
|
|
|
|
|
Args:
|
|
|
|
|
out: String message to be logged.
|
|
|
|
|
|
|
|
|
|
Examples:
|
|
|
|
|
```logger.critical("Hello from query module.")```
|
|
|
|
|
"""
|
|
|
|
|
self._logger.critical(out)
|
|
|
|
|
|
|
|
|
|
def error(self, out: str) -> None:
|
|
|
|
|
"""
|
|
|
|
|
Log message on ERROR level..
|
|
|
|
|
Args:
|
|
|
|
|
out: String message to be logged.
|
|
|
|
|
|
|
|
|
|
Examples:
|
|
|
|
|
```logger.error("Hello from query module.")```
|
|
|
|
|
"""
|
|
|
|
|
self._logger.error(out)
|
|
|
|
|
|
|
|
|
|
def trace(self, out: str) -> None:
|
|
|
|
|
"""
|
|
|
|
|
Log message on TRACE level..
|
|
|
|
|
Args:
|
|
|
|
|
out: String message to be logged.
|
|
|
|
|
|
|
|
|
|
Examples:
|
|
|
|
|
```logger.trace("Hello from query module.")```
|
|
|
|
|
"""
|
|
|
|
|
self._logger.trace(out)
|
|
|
|
|
|
|
|
|
|
def debug(self, out: str) -> None:
|
|
|
|
|
"""
|
|
|
|
|
Log message on DEBUG level..
|
|
|
|
|
Args:
|
|
|
|
|
out: String message to be logged.
|
|
|
|
|
|
|
|
|
|
Examples:
|
|
|
|
|
```logger.debug("Hello from query module.")```
|
|
|
|
|
"""
|
|
|
|
|
self._logger.debug(out)
|
|
|
|
|
|
|
|
|
|
|
2021-10-02 19:09:00 +08:00
|
|
|
|
_wrap_exceptions()
|