Stub out full Python API for Query Modules
Summary: This is the Python 3.5+ version of our C API found in include/mg_procedure.h. It should strive to be simple to use, but very hard to misuse. The Python API avoids almost all memory management issues found in the C API, so it is automatically less error prone. Unfortunately, we cannot avoid the issue of having some objects being alive only during the execution of a custom procedure. For such objects we have a `Invalid<Object>Error` exception types. The Python API should fairly easily check the validity because all such objects are related to ProcCtx in some way. Our C/C++ part will push 1 instance of ProcCtx each time a Python procedure is invoked and can therefore easily change ProcCtx to be invalid. This can be as simple as changing the implementation pointer of ProcCtx to NULL. Reviewers: mferencevic, ipaljak, tlastre, dsantl, buda Reviewed By: mferencevic, tlastre Subscribers: pullbot Differential Revision: https://phabricator.memgraph.io/D2650
This commit is contained in:
parent
6c422f3208
commit
4281a57923
355
include/mgp.py
Normal file
355
include/mgp.py
Normal file
@ -0,0 +1,355 @@
|
||||
'''
|
||||
This module provides the API for usage in custom openCypher procedures.
|
||||
'''
|
||||
|
||||
# C API using `mgp_memory` is not exposed in Python, instead the usage of such
|
||||
# API is hidden behind Python API. Any function requiring an instance of
|
||||
# `mgp_memory` should go through a `ProcCtx` instance.
|
||||
#
|
||||
# `mgp_value` does not exist as such in Python, instead all `mgp_value`
|
||||
# instances are marshalled to an appropriate Python object. This implies that
|
||||
# `mgp_list` and `mgp_map` are mapped to `list` and `dict` respectively.
|
||||
#
|
||||
# Only the public API is stubbed out here. Any private details are left for the
|
||||
# actual implementation. Functions have type annotations as supported by Python
|
||||
# 3.5, but variable type annotations are only available with Python 3.6+
|
||||
|
||||
from collections import namedtuple
|
||||
import typing
|
||||
|
||||
|
||||
class Label:
|
||||
'''Label of a Vertex.'''
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
pass
|
||||
|
||||
|
||||
class Labels:
|
||||
'''A collection of labels on a Vertex.'''
|
||||
|
||||
def __len__(self) -> int:
|
||||
'''Raise InvalidVertexError.'''
|
||||
pass
|
||||
|
||||
def __getitem__(self, index: int) -> Label:
|
||||
'''Raise InvalidVertexError.'''
|
||||
pass
|
||||
|
||||
def __iter__(self) -> typing.Iterable[Label]:
|
||||
'''Raise InvalidVertexError.'''
|
||||
pass
|
||||
|
||||
def __contains__(self, label: typing.Union[Label, str]) -> bool:
|
||||
'''Test whether a label exists, either by name or another Label.
|
||||
|
||||
Raise InvalidVertexError.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
# Named property value of a Vertex or an Edge.
|
||||
# It would be better to use typing.NamedTuple with typed fields, but that is
|
||||
# not available in Python 3.5.
|
||||
Property = namedtuple('Property', ('name', 'value'))
|
||||
|
||||
|
||||
class Properties:
|
||||
'''A collection of properties either on a Vertex or an Edge.'''
|
||||
|
||||
def get(self, property_name: str, default=None) -> object:
|
||||
'''Get the value of a property with the given name or return default.
|
||||
|
||||
Raise InvalidEdgeError or InvalidVertexError.
|
||||
'''
|
||||
pass
|
||||
|
||||
def items(self) -> typing.Iterable[Property]:
|
||||
'''Raise InvalidEdgeError or InvalidVertexError.'''
|
||||
pass
|
||||
|
||||
def keys(self) -> typing.Iterable[str]:
|
||||
'''Iterate over property names.
|
||||
|
||||
Raise InvalidEdgeError or InvalidVertexError.
|
||||
'''
|
||||
pass
|
||||
|
||||
def values(self) -> typing.Iterable[object]:
|
||||
'''Iterate over property values.
|
||||
|
||||
Raise InvalidEdgeError or InvalidVertexError.
|
||||
'''
|
||||
pass
|
||||
|
||||
def __len__(self) -> int:
|
||||
'''Raise InvalidEdgeError or InvalidVertexError.'''
|
||||
pass
|
||||
|
||||
def __iter__(self) -> typing.Iterable[str]:
|
||||
'''Iterate over property names.
|
||||
|
||||
Raise InvalidEdgeError or InvalidVertexError.
|
||||
'''
|
||||
pass
|
||||
|
||||
def __getitem__(self, property_name: str) -> object:
|
||||
'''Get the value of a property with the given name or raise KeyError.
|
||||
|
||||
Raise InvalidEdgeError or InvalidVertexError.'''
|
||||
pass
|
||||
|
||||
def __contains__(self, property_name: str) -> bool:
|
||||
pass
|
||||
|
||||
|
||||
class EdgeType:
|
||||
'''Type of an Edge.'''
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
pass
|
||||
|
||||
|
||||
class InvalidEdgeError(Exception):
|
||||
'''Signals using an Edge instance not part of the procedure context.'''
|
||||
pass
|
||||
|
||||
|
||||
class Edge:
|
||||
'''Edge in the graph database.
|
||||
|
||||
Access to an Edge is only valid during a single execution of a procedure in
|
||||
a query. You should not globally store an instance of an Edge. Using an
|
||||
invalid Edge instance will raise InvalidEdgeError.
|
||||
'''
|
||||
|
||||
@property
|
||||
def type(self) -> EdgeType:
|
||||
'''Raise InvalidEdgeError.'''
|
||||
pass
|
||||
|
||||
@property
|
||||
def from_vertex(self): # -> Vertex:
|
||||
'''Raise InvalidEdgeError.'''
|
||||
pass
|
||||
|
||||
@property
|
||||
def to_vertex(self): # -> Vertex:
|
||||
'''Raise InvalidEdgeError.'''
|
||||
pass
|
||||
|
||||
@property
|
||||
def properties(self) -> Properties:
|
||||
'''Raise InvalidEdgeError.'''
|
||||
pass
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
'''Raise InvalidEdgeError.'''
|
||||
pass
|
||||
|
||||
|
||||
VertexId = typing.NewType('VertexId', int)
|
||||
|
||||
|
||||
class InvalidVertexError(Exception):
|
||||
'''Signals using a Vertex instance not part of the procedure context.'''
|
||||
pass
|
||||
|
||||
|
||||
class Vertex:
|
||||
'''Vertex in the graph database.
|
||||
|
||||
Access to a Vertex is only valid during a single execution of a procedure
|
||||
in a query. You should not globally store an instance of a Vertex. Using an
|
||||
invalid Vertex instance will raise InvalidVertexError.
|
||||
'''
|
||||
|
||||
@property
|
||||
def id(self) -> VertexId:
|
||||
'''Raise InvalidVertexError.'''
|
||||
pass
|
||||
|
||||
@property
|
||||
def labels(self) -> Labels:
|
||||
'''Raise InvalidVertexError.'''
|
||||
pass
|
||||
|
||||
@property
|
||||
def properties(self) -> Properties:
|
||||
'''Raise InvalidVertexError.'''
|
||||
pass
|
||||
|
||||
@property
|
||||
def in_edges(self) -> typing.Iterable[Edge]:
|
||||
'''Raise InvalidVertexError.'''
|
||||
pass
|
||||
|
||||
@property
|
||||
def out_edges(self) -> typing.Iterable[Edge]:
|
||||
'''Raise InvalidVertexError.'''
|
||||
pass
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
'''Raise InvalidVertexError.'''
|
||||
pass
|
||||
|
||||
|
||||
class Path:
|
||||
'''Path containing Vertex and Edge instances.'''
|
||||
|
||||
def __init__(self, starting_vertex: Vertex):
|
||||
'''Initialize with a starting Vertex.
|
||||
|
||||
Raise InvalidVertexError if passed in Vertex is invalid.
|
||||
'''
|
||||
pass
|
||||
|
||||
def expand(self, edge: Edge):
|
||||
'''Append an edge continuing from the last vertex on the path.
|
||||
|
||||
The last vertex on the path will become the other endpoint of the given
|
||||
edge, as continued from the current last vertex.
|
||||
|
||||
Raise ValueError if the current last vertex in the path is not part of
|
||||
the given edge.
|
||||
Raise InvalidEdgeError if passed in edge is invalid.
|
||||
'''
|
||||
pass
|
||||
|
||||
@property
|
||||
def vertices(self) -> typing.Tuple[Vertex, ...]:
|
||||
'''Vertices ordered from the start to the end of the path.'''
|
||||
pass
|
||||
|
||||
@property
|
||||
def edges(self) -> typing.Tuple[Edge, ...]:
|
||||
'''Edges ordered from the start to the end of the path.'''
|
||||
pass
|
||||
|
||||
|
||||
class Record:
|
||||
'''Represents a record of resulting field values.'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
'''Initialize with name=value fields in kwargs.'''
|
||||
pass
|
||||
|
||||
|
||||
class InvalidProcCtxError(Exception):
|
||||
'''Signals using an ProcCtx instance outside of the registered procedure.'''
|
||||
pass
|
||||
|
||||
|
||||
class Vertices:
|
||||
'''Iterable over vertices in a graph.'''
|
||||
|
||||
def __iter__(self) -> typing.Iterable[Vertex]:
|
||||
'''Raise InvalidProcCtxError if context is invalid.'''
|
||||
pass
|
||||
|
||||
|
||||
class Graph:
|
||||
'''State of the graph database in current ProcCtx.'''
|
||||
|
||||
def get_vertex_by_id(self, vertex_id: VertexId) -> Vertex:
|
||||
'''Return the Vertex corresponding to given vertex_id from the graph.
|
||||
|
||||
Access to a Vertex is only valid during a single execution of a
|
||||
procedure in a query. You should not globally store the returned
|
||||
Vertex.
|
||||
|
||||
Raise IndexError if unable to find the given vertex_id.
|
||||
Raise InvalidProcCtxError if context is invalid.
|
||||
'''
|
||||
pass
|
||||
|
||||
@property
|
||||
def vertices(self) -> Vertices:
|
||||
'''All vertices in the graph.
|
||||
|
||||
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.
|
||||
|
||||
Raise InvalidProcCtxError if context is invalid.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class ProcCtx:
|
||||
'''Context of a procedure being executed.
|
||||
|
||||
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.
|
||||
'''
|
||||
|
||||
@property
|
||||
def graph(self) -> Graph:
|
||||
'''Raise InvalidProcCtxError if context is invalid.'''
|
||||
pass
|
||||
|
||||
|
||||
# Additional typing support
|
||||
|
||||
Number = typing.Union[int, float]
|
||||
|
||||
List = typing.List
|
||||
|
||||
Map = typing.Union[dict, Edge, Vertex]
|
||||
|
||||
Any = typing.Union[bool, str, Number, Map, Path, list]
|
||||
|
||||
Nullable = typing.Optional
|
||||
|
||||
|
||||
# Procedure registration
|
||||
|
||||
class Deprecated:
|
||||
'''Annotate a resulting Record's field as deprecated.'''
|
||||
def __init__(self, type_):
|
||||
pass
|
||||
|
||||
|
||||
def read_proc(func: typing.Callable[..., Record]):
|
||||
'''
|
||||
Register `func` as a a read-only procedure of the current module.
|
||||
|
||||
`read_proc` is meant to be used as a decorator function to register module
|
||||
procedures. The registered `func` needs to be a callable which optionally
|
||||
takes `ProcCtx` as the first argument. Other arguments of `func` will be
|
||||
bound to values passed in the cypherQuery. The full signature of `func`
|
||||
needs to be annotated with types. The return type must be
|
||||
`Record(field_name=type, ...)` and the procedure must produce either a
|
||||
complete Record or None. To mark a field as deprecated, use
|
||||
`Record(field_name=Deprecated(type), ...)`. Multiple records can be
|
||||
produced by returning an iterable of them. Registering generator functions
|
||||
is currently not supported.
|
||||
|
||||
Example usage.
|
||||
|
||||
```
|
||||
import mgp
|
||||
|
||||
@mgp.read_proc
|
||||
def procedure(context: mgp.ProcCtx,
|
||||
required_arg: mgp.Nullable[mgp.Any],
|
||||
optional_arg: mgp.Nullable[mgp.Any] = None
|
||||
) -> mgp.Record(result=str, args=list):
|
||||
args = [required_arg, optional_arg]
|
||||
# Multiple rows can be produced by returning an iterable of mgp.Record
|
||||
return mgp.Record(args=args, result='Hello World!')
|
||||
```
|
||||
|
||||
The example procedure above returns 2 fields: `args` and `result`.
|
||||
* `args` is a copy of arguments passed to the procedure.
|
||||
* `result` is the result of this procedure, a "Hello World!" string.
|
||||
Any errors can be reported by raising an Exception.
|
||||
|
||||
The procedure can be invoked in openCypher using the following calls:
|
||||
CALL example.procedure(1, 2) YIELD args, result;
|
||||
CALL example.procedure(1) YIELD args, result;
|
||||
Naturally, you may pass in different arguments or yield less fields.
|
||||
'''
|
||||
pass
|
Loading…
Reference in New Issue
Block a user