Add python API for messages transformations (#181)

* Add python messages/transformations implementation

* Added fixed result return type to transformations

* Added is_deprecated to mgp_trans
This commit is contained in:
Kostas Kyrimis
2021-07-01 13:36:41 +03:00
committed by Antonio Andelic
parent ac230d0c2d
commit a928c158da
8 changed files with 516 additions and 47 deletions

View File

@@ -714,6 +714,18 @@ class Deprecated:
self.field_type = type_
def raise_if_does_not_meet_requirements(func: typing.Callable[..., Record]):
if not callable(func):
raise TypeError("Expected a callable object, got an instance of '{}'"
.format(type(func)))
if inspect.iscoroutinefunction(func):
raise TypeError("Callable must not be 'async def' function")
if sys.version_info >= (3, 6):
if inspect.isasyncgenfunction(func):
raise TypeError("Callable must not be 'async def' function")
if inspect.isgeneratorfunction(func):
raise NotImplementedError("Generator functions are not supported")
def read_proc(func: typing.Callable[..., Record]):
'''
Register `func` as a a read-only procedure of the current module.
@@ -754,16 +766,7 @@ def read_proc(func: typing.Callable[..., Record]):
CALL example.procedure(1) YIELD args, result;
Naturally, you may pass in different arguments or yield less fields.
'''
if not callable(func):
raise TypeError("Expected a callable object, got an instance of '{}'"
.format(type(func)))
if inspect.iscoroutinefunction(func):
raise TypeError("Callable must not be 'async def' function")
if sys.version_info >= (3, 6):
if inspect.isasyncgenfunction(func):
raise TypeError("Callable must not be 'async def' function")
if inspect.isgeneratorfunction(func):
raise NotImplementedError("Generator functions are not supported")
raise_if_does_not_meet_requirements(func)
sig = inspect.signature(func)
params = tuple(sig.parameters.values())
if params and params[0].annotation is ProcCtx:
@@ -799,3 +802,123 @@ def read_proc(func: typing.Callable[..., Record]):
else:
mgp_proc.add_result(name, _typing_to_cypher_type(type_))
return func
class InvalidMessageError(Exception):
'''Signals using a message instance outside of the registered transformation.'''
pass
class Message:
'''Represents a message from a stream.'''
__slots__ = ('_message',)
def __init__(self, message):
if not isinstance(message, _mgp.Message):
raise TypeError("Expected '_mgp.Message', got '{}'".format(type(message)))
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:
'''Return True if `self` is in valid context and may be used.'''
return self._message.is_valid()
def payload(self) -> bytes:
if not self.is_valid():
raise InvalidMessageError()
return self._messages._payload(_message)
def topic_name(self) -> str:
if not self.is_valid():
raise InvalidMessageError()
return self._messages._topic_name(_message)
def key() -> bytes:
if not self.is_valid():
raise InvalidMessageError()
return self._messages.key(_message)
def timestamp() -> int:
if not self.is_valid():
raise InvalidMessageError()
return self._messages.timestamp(_message)
class InvalidMessagesError(Exception):
'''Signals using a messages instance outside of the registered transformation.'''
pass
class Messages:
'''Represents a list of messages from a stream.'''
__slots__ = ('_messages',)
def __init__(self, messages):
if not isinstance(messages, _mgp.Messages):
raise TypeError("Expected '_mgp.Messages', got '{}'".format(type(messages)))
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:
'''Return True if `self` is in valid context and may be used.'''
return self._messages.is_valid()
def message_at(self, id : int) -> Message:
'''Raise InvalidMessagesError if context is invalid.'''
if not self.is_valid():
raise InvalidMessagesError()
return Message(self._messages.message_at(id))
def total_messages() -> int:
'''Raise InvalidContextError if context is invalid.'''
if not self.is_valid():
raise InvalidMessagesError()
return self._messages.total_messages()
class TransCtx:
'''Context of a transformation being executed.
Access to a TransCtx is only valid during a single execution of a transformation.
You should not globally store a TransCtx instance.
'''
__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()
@property
def graph(self) -> Graph:
'''Raise InvalidContextError if context is invalid.'''
if not self.is_valid():
raise InvalidContextError()
return self._graph
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:
raise NotImplementedError("Valid signatures for transformations are (TransCtx, Messages) or (Messages)")
if params[0].annotation is TransCtx:
@functools.wraps(func)
def wrapper(graph, messages):
return func(TransCtx(graph), messages)
_mgp._MODULE.add_transformation(wrapper)
else:
@functools.wraps(func)
def wrapper(graph, messages):
return func(messages)
_mgp._MODULE.add_transformation(wrapper)
return func