2650 lines
101 KiB
C++
2650 lines
101 KiB
C++
// Copyright 2022 Memgraph Ltd.
|
|
//
|
|
// Use of this software is governed by the Business Source License
|
|
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
|
// License, and you may not use this file except in compliance with the Business Source License.
|
|
//
|
|
// As of the Change Date specified in that file, in accordance with
|
|
// the Business Source License, use of this software will be governed
|
|
// by the Apache License, Version 2.0, included in the file
|
|
// licenses/APL.txt.
|
|
|
|
#include "query/procedure/py_module.hpp"
|
|
|
|
#include <datetime.h>
|
|
#include <pyerrors.h>
|
|
#include <array>
|
|
#include <sstream>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <string_view>
|
|
|
|
#include "mg_procedure.h"
|
|
#include "query/procedure/mg_procedure_helpers.hpp"
|
|
#include "query/procedure/mg_procedure_impl.hpp"
|
|
#include "utils/memory.hpp"
|
|
#include "utils/on_scope_exit.hpp"
|
|
#include "utils/pmr/vector.hpp"
|
|
|
|
namespace memgraph::query::procedure {
|
|
|
|
namespace {
|
|
// Set this as a __reduce__ special method on our types to prevent `pickle` and
|
|
// `copy` module operations on our types.
|
|
PyObject *DisallowPickleAndCopy(PyObject *self, PyObject *Py_UNUSED(ignored)) {
|
|
auto *type = Py_TYPE(self);
|
|
std::stringstream ss;
|
|
ss << "cannot pickle nor copy '" << type->tp_name << "' object";
|
|
const auto &msg = ss.str();
|
|
PyErr_SetString(PyExc_TypeError, msg.c_str());
|
|
return nullptr;
|
|
}
|
|
|
|
PyObject *gMgpUnknownError{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
|
PyObject *gMgpUnableToAllocateError{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
|
PyObject *gMgpInsufficientBufferError{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
|
PyObject *gMgpOutOfRangeError{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
|
PyObject *gMgpLogicErrorError{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
|
PyObject *gMgpDeletedObjectError{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
|
PyObject *gMgpInvalidArgumentError{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
|
PyObject *gMgpKeyAlreadyExistsError{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
|
PyObject *gMgpImmutableObjectError{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
|
PyObject *gMgpValueConversionError{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
|
PyObject *gMgpSerializationError{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
|
|
|
// Returns true if an exception is raised
|
|
bool RaiseExceptionFromErrorCode(const mgp_error error) {
|
|
switch (error) {
|
|
case mgp_error::MGP_ERROR_NO_ERROR:
|
|
return false;
|
|
case mgp_error::MGP_ERROR_UNKNOWN_ERROR: {
|
|
PyErr_SetString(gMgpUnknownError, "Unknown error happened.");
|
|
return true;
|
|
}
|
|
case mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE: {
|
|
PyErr_SetString(gMgpUnableToAllocateError, "Unable to allocate memory.");
|
|
return true;
|
|
}
|
|
case mgp_error::MGP_ERROR_INSUFFICIENT_BUFFER: {
|
|
PyErr_SetString(gMgpInsufficientBufferError, "Insufficient buffer.");
|
|
return true;
|
|
}
|
|
case mgp_error::MGP_ERROR_OUT_OF_RANGE: {
|
|
PyErr_SetString(gMgpOutOfRangeError, "Out of range.");
|
|
return true;
|
|
}
|
|
case mgp_error::MGP_ERROR_LOGIC_ERROR: {
|
|
PyErr_SetString(gMgpLogicErrorError, "Logic error.");
|
|
return true;
|
|
}
|
|
case mgp_error::MGP_ERROR_DELETED_OBJECT: {
|
|
PyErr_SetString(gMgpDeletedObjectError, "Accessing deleted object.");
|
|
return true;
|
|
}
|
|
case mgp_error::MGP_ERROR_INVALID_ARGUMENT: {
|
|
PyErr_SetString(gMgpInvalidArgumentError, "Invalid argument.");
|
|
return true;
|
|
}
|
|
case mgp_error::MGP_ERROR_KEY_ALREADY_EXISTS: {
|
|
PyErr_SetString(gMgpKeyAlreadyExistsError, "Key already exists.");
|
|
return true;
|
|
}
|
|
case mgp_error::MGP_ERROR_IMMUTABLE_OBJECT: {
|
|
PyErr_SetString(gMgpImmutableObjectError, "Cannot modify immutable object.");
|
|
return true;
|
|
}
|
|
case mgp_error::MGP_ERROR_VALUE_CONVERSION: {
|
|
PyErr_SetString(gMgpValueConversionError, "Value conversion failed.");
|
|
return true;
|
|
}
|
|
case mgp_error::MGP_ERROR_SERIALIZATION_ERROR: {
|
|
PyErr_SetString(gMgpSerializationError, "Operation cannot be serialized.");
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
mgp_value *PyObjectToMgpValueWithPythonExceptions(PyObject *py_value, mgp_memory *memory) noexcept {
|
|
try {
|
|
return PyObjectToMgpValue(py_value, memory);
|
|
} catch (const std::bad_alloc &e) {
|
|
PyErr_SetString(PyExc_MemoryError, e.what());
|
|
return nullptr;
|
|
} catch (const std::overflow_error &e) {
|
|
PyErr_SetString(PyExc_OverflowError, e.what());
|
|
return nullptr;
|
|
} catch (const std::invalid_argument &e) {
|
|
PyErr_SetString(PyExc_ValueError, e.what());
|
|
return nullptr;
|
|
} catch (const std::exception &e) {
|
|
PyErr_SetString(PyExc_RuntimeError, e.what());
|
|
return nullptr;
|
|
} catch (...) {
|
|
PyErr_SetString(PyExc_RuntimeError, "Unknown error happened");
|
|
return nullptr;
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
// Definitions of types wrapping C API types
|
|
//
|
|
// These should all be in the private `_mgp` Python module, which will be used
|
|
// by the `mgp` to implement the user friendly Python API.
|
|
|
|
// Wraps mgp_graph in a PyObject.
|
|
//
|
|
// Executing a `CALL python_module.procedure(...)` in openCypher should
|
|
// instantiate exactly 1 mgp_graph instance. We will rely on this assumption in
|
|
// order to test for validity of usage. The idea is to clear the `graph` to
|
|
// `nullptr` after the execution completes. If a user stored a reference to
|
|
// `_mgp.Graph` in their global Python state, then we are no longer working with
|
|
// a valid graph so `nullptr` will catch this. `_mgp.Graph` provides `is_valid`
|
|
// method for checking this by our higher level API in `mgp` module. Python only
|
|
// does shallow copies by default, and we do not provide deep copy of
|
|
// `_mgp.Graph`, so this validity concept should work fine.
|
|
//
|
|
// clang-format off
|
|
struct PyGraph {
|
|
PyObject_HEAD
|
|
mgp_graph *graph;
|
|
mgp_memory *memory;
|
|
};
|
|
// clang-format on
|
|
|
|
// clang-format off
|
|
struct PyVerticesIterator {
|
|
PyObject_HEAD
|
|
mgp_vertices_iterator *it;
|
|
PyGraph *py_graph;
|
|
};
|
|
// clang-format on
|
|
|
|
PyObject *MakePyVertex(mgp_vertex &vertex, PyGraph *py_graph);
|
|
|
|
void PyVerticesIteratorDealloc(PyVerticesIterator *self) {
|
|
MG_ASSERT(self->it);
|
|
MG_ASSERT(self->py_graph);
|
|
// Avoid invoking `mgp_vertices_iterator_destroy` if we are not in valid
|
|
// execution context. The query execution should free all memory used during
|
|
// execution, so we may cause a double free issue.
|
|
if (self->py_graph->graph) mgp_vertices_iterator_destroy(self->it);
|
|
Py_DECREF(self->py_graph);
|
|
Py_TYPE(self)->tp_free(self);
|
|
}
|
|
|
|
PyObject *PyVerticesIteratorGet(PyVerticesIterator *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(self->it);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
mgp_vertex *vertex{nullptr};
|
|
if (RaiseExceptionFromErrorCode(mgp_vertices_iterator_get(self->it, &vertex))) {
|
|
return nullptr;
|
|
}
|
|
if (vertex == nullptr) {
|
|
Py_INCREF(Py_None);
|
|
return Py_None;
|
|
}
|
|
return MakePyVertex(*vertex, self->py_graph);
|
|
}
|
|
|
|
PyObject *PyVerticesIteratorNext(PyVerticesIterator *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(self->it);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
mgp_vertex *vertex{nullptr};
|
|
if (RaiseExceptionFromErrorCode(mgp_vertices_iterator_next(self->it, &vertex))) {
|
|
return nullptr;
|
|
}
|
|
if (vertex == nullptr) {
|
|
Py_INCREF(Py_None);
|
|
return Py_None;
|
|
}
|
|
return MakePyVertex(*vertex, self->py_graph);
|
|
}
|
|
|
|
static PyMethodDef PyVerticesIteratorMethods[] = {
|
|
{"__reduce__", reinterpret_cast<PyCFunction>(DisallowPickleAndCopy), METH_NOARGS, "__reduce__ is not supported"},
|
|
{"get", reinterpret_cast<PyCFunction>(PyVerticesIteratorGet), METH_NOARGS,
|
|
"Get the current vertex pointed to by the iterator or return None."},
|
|
{"next", reinterpret_cast<PyCFunction>(PyVerticesIteratorNext), METH_NOARGS,
|
|
"Advance the iterator to the next vertex and return it."},
|
|
{nullptr},
|
|
};
|
|
|
|
// clang-format off
|
|
static PyTypeObject PyVerticesIteratorType = {
|
|
PyVarObject_HEAD_INIT(nullptr, 0)
|
|
.tp_name = "_mgp.VerticesIterator",
|
|
.tp_basicsize = sizeof(PyVerticesIterator),
|
|
.tp_dealloc = reinterpret_cast<destructor>(PyVerticesIteratorDealloc),
|
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
|
.tp_doc = "Wraps struct mgp_vertices_iterator.",
|
|
.tp_methods = PyVerticesIteratorMethods,
|
|
};
|
|
// clang-format on
|
|
|
|
// clang-format off
|
|
struct PyEdgesIterator {
|
|
PyObject_HEAD
|
|
mgp_edges_iterator *it;
|
|
PyGraph *py_graph;
|
|
};
|
|
// clang-format on
|
|
|
|
PyObject *MakePyEdge(mgp_edge &edge, PyGraph *py_graph);
|
|
|
|
void PyEdgesIteratorDealloc(PyEdgesIterator *self) {
|
|
MG_ASSERT(self->it);
|
|
MG_ASSERT(self->py_graph);
|
|
// Avoid invoking `mgp_edges_iterator_destroy` if we are not in valid
|
|
// execution context. The query execution should free all memory used during
|
|
// execution, so we may cause a double free issue.
|
|
if (self->py_graph->graph) mgp_edges_iterator_destroy(self->it);
|
|
Py_DECREF(self->py_graph);
|
|
Py_TYPE(self)->tp_free(self);
|
|
}
|
|
|
|
PyObject *PyEdgesIteratorGet(PyEdgesIterator *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(self->it);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
mgp_edge *edge{nullptr};
|
|
if (RaiseExceptionFromErrorCode(mgp_edges_iterator_get(self->it, &edge))) {
|
|
return nullptr;
|
|
}
|
|
if (edge == nullptr) {
|
|
Py_INCREF(Py_None);
|
|
return Py_None;
|
|
}
|
|
return MakePyEdge(*edge, self->py_graph);
|
|
}
|
|
|
|
PyObject *PyEdgesIteratorNext(PyEdgesIterator *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(self->it);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
mgp_edge *edge{nullptr};
|
|
if (RaiseExceptionFromErrorCode(mgp_edges_iterator_next(self->it, &edge))) {
|
|
return nullptr;
|
|
}
|
|
if (edge == nullptr) {
|
|
Py_INCREF(Py_None);
|
|
return Py_None;
|
|
}
|
|
return MakePyEdge(*edge, self->py_graph);
|
|
}
|
|
|
|
static PyMethodDef PyEdgesIteratorMethods[] = {
|
|
{"__reduce__", reinterpret_cast<PyCFunction>(DisallowPickleAndCopy), METH_NOARGS, "__reduce__ is not supported"},
|
|
{"get", reinterpret_cast<PyCFunction>(PyEdgesIteratorGet), METH_NOARGS,
|
|
"Get the current edge pointed to by the iterator or return None."},
|
|
{"next", reinterpret_cast<PyCFunction>(PyEdgesIteratorNext), METH_NOARGS,
|
|
"Advance the iterator to the next edge and return it."},
|
|
{nullptr},
|
|
};
|
|
|
|
// clang-format off
|
|
static PyTypeObject PyEdgesIteratorType = {
|
|
PyVarObject_HEAD_INIT(nullptr, 0)
|
|
.tp_name = "_mgp.EdgesIterator",
|
|
.tp_basicsize = sizeof(PyEdgesIterator),
|
|
.tp_dealloc = reinterpret_cast<destructor>(PyEdgesIteratorDealloc),
|
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
|
.tp_doc = "Wraps struct mgp_edges_iterator.",
|
|
.tp_methods = PyEdgesIteratorMethods,
|
|
};
|
|
// clang-format on
|
|
|
|
PyObject *PyGraphInvalidate(PyGraph *self, PyObject *Py_UNUSED(ignored)) {
|
|
self->graph = nullptr;
|
|
self->memory = nullptr;
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
bool PyGraphIsValidImpl(PyGraph &self) { return self.graph != nullptr; }
|
|
|
|
PyObject *PyGraphIsValid(PyGraph *self, PyObject *Py_UNUSED(ignored)) {
|
|
return PyBool_FromLong(PyGraphIsValidImpl(*self));
|
|
}
|
|
|
|
PyObject *PyGraphIsMutable(PyGraph *self, PyObject *Py_UNUSED(ignored)) {
|
|
return PyBool_FromLong(CallBool(mgp_graph_is_mutable, self->graph));
|
|
}
|
|
|
|
PyObject *MakePyVertexWithoutCopy(mgp_vertex &vertex, PyGraph *py_graph);
|
|
|
|
PyObject *PyGraphGetVertexById(PyGraph *self, PyObject *args) {
|
|
MG_ASSERT(PyGraphIsValidImpl(*self));
|
|
MG_ASSERT(self->memory);
|
|
static_assert(std::is_same_v<int64_t, long>);
|
|
int64_t id = 0;
|
|
if (!PyArg_ParseTuple(args, "l", &id)) return nullptr;
|
|
mgp_vertex *vertex{nullptr};
|
|
if (RaiseExceptionFromErrorCode(mgp_graph_get_vertex_by_id(self->graph, mgp_vertex_id{id}, self->memory, &vertex))) {
|
|
return nullptr;
|
|
}
|
|
if (!vertex) {
|
|
PyErr_SetString(PyExc_IndexError, "Unable to find the vertex with given ID.");
|
|
return nullptr;
|
|
}
|
|
auto *py_vertex = MakePyVertexWithoutCopy(*vertex, self);
|
|
if (!py_vertex) mgp_vertex_destroy(vertex);
|
|
return py_vertex;
|
|
}
|
|
|
|
PyObject *PyGraphCreateVertex(PyGraph *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(PyGraphIsValidImpl(*self));
|
|
MG_ASSERT(self->memory);
|
|
MgpUniquePtr<mgp_vertex> new_vertex{nullptr, mgp_vertex_destroy};
|
|
if (RaiseExceptionFromErrorCode(CreateMgpObject(new_vertex, mgp_graph_create_vertex, self->graph, self->memory))) {
|
|
return nullptr;
|
|
}
|
|
auto *py_vertex = MakePyVertexWithoutCopy(*new_vertex, self);
|
|
if (py_vertex != nullptr) {
|
|
static_cast<void>(new_vertex.release());
|
|
}
|
|
return py_vertex;
|
|
}
|
|
|
|
PyObject *PyGraphCreateEdge(PyGraph *self, PyObject *args);
|
|
|
|
PyObject *PyGraphDeleteVertex(PyGraph *self, PyObject *args);
|
|
|
|
PyObject *PyGraphDetachDeleteVertex(PyGraph *self, PyObject *args);
|
|
|
|
PyObject *PyGraphDeleteEdge(PyGraph *self, PyObject *args);
|
|
|
|
PyObject *PyGraphIterVertices(PyGraph *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(PyGraphIsValidImpl(*self));
|
|
MG_ASSERT(self->memory);
|
|
mgp_vertices_iterator *vertices_it{nullptr};
|
|
if (RaiseExceptionFromErrorCode(mgp_graph_iter_vertices(self->graph, self->memory, &vertices_it))) {
|
|
return nullptr;
|
|
}
|
|
auto *py_vertices_it = PyObject_New(PyVerticesIterator, &PyVerticesIteratorType);
|
|
if (!py_vertices_it) {
|
|
mgp_vertices_iterator_destroy(vertices_it);
|
|
return nullptr;
|
|
}
|
|
py_vertices_it->it = vertices_it;
|
|
Py_INCREF(self);
|
|
py_vertices_it->py_graph = self;
|
|
return reinterpret_cast<PyObject *>(py_vertices_it);
|
|
}
|
|
|
|
PyObject *PyGraphMustAbort(PyGraph *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(PyGraphIsValidImpl(*self));
|
|
return PyBool_FromLong(mgp_must_abort(self->graph));
|
|
}
|
|
|
|
static PyMethodDef PyGraphMethods[] = {
|
|
{"__reduce__", reinterpret_cast<PyCFunction>(DisallowPickleAndCopy), METH_NOARGS, "__reduce__ is not supported"},
|
|
{"invalidate", reinterpret_cast<PyCFunction>(PyGraphInvalidate), METH_NOARGS,
|
|
"Invalidate the Graph context thus preventing the Graph from being used."},
|
|
{"is_valid", reinterpret_cast<PyCFunction>(PyGraphIsValid), METH_NOARGS,
|
|
"Return True if Graph is in valid context and may be used."},
|
|
{"is_mutable", reinterpret_cast<PyCFunction>(PyGraphIsMutable), METH_NOARGS,
|
|
"Return True if Graph is mutable and can be used to modify vertices and edges."},
|
|
{"get_vertex_by_id", reinterpret_cast<PyCFunction>(PyGraphGetVertexById), METH_VARARGS,
|
|
"Get the vertex or raise IndexError."},
|
|
{"create_vertex", reinterpret_cast<PyCFunction>(PyGraphCreateVertex), METH_NOARGS, "Create a vertex."},
|
|
{"create_edge", reinterpret_cast<PyCFunction>(PyGraphCreateEdge), METH_VARARGS, "Create an edge."},
|
|
{"delete_vertex", reinterpret_cast<PyCFunction>(PyGraphDeleteVertex), METH_VARARGS, "Delete a vertex."},
|
|
{"detach_delete_vertex", reinterpret_cast<PyCFunction>(PyGraphDetachDeleteVertex), METH_VARARGS,
|
|
"Delete a vertex and all of its edges."},
|
|
{"delete_edge", reinterpret_cast<PyCFunction>(PyGraphDeleteEdge), METH_VARARGS, "Delete an edge."},
|
|
{"iter_vertices", reinterpret_cast<PyCFunction>(PyGraphIterVertices), METH_NOARGS, "Return _mgp.VerticesIterator."},
|
|
{"must_abort", reinterpret_cast<PyCFunction>(PyGraphMustAbort), METH_NOARGS,
|
|
"Check whether the running procedure should abort"},
|
|
{nullptr},
|
|
};
|
|
|
|
// clang-format off
|
|
static PyTypeObject PyGraphType = {
|
|
PyVarObject_HEAD_INIT(nullptr, 0)
|
|
.tp_name = "_mgp.Graph",
|
|
.tp_basicsize = sizeof(PyGraph),
|
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
|
.tp_doc = "Wraps struct mgp_graph.",
|
|
.tp_methods = PyGraphMethods,
|
|
};
|
|
// clang-format on
|
|
|
|
PyObject *MakePyGraph(mgp_graph *graph, mgp_memory *memory) {
|
|
MG_ASSERT(!graph || (graph && memory));
|
|
auto *py_graph = PyObject_New(PyGraph, &PyGraphType);
|
|
if (!py_graph) return nullptr;
|
|
py_graph->graph = graph;
|
|
py_graph->memory = memory;
|
|
return reinterpret_cast<PyObject *>(py_graph);
|
|
}
|
|
|
|
// clang-format off
|
|
struct PyCypherType {
|
|
PyObject_HEAD
|
|
mgp_type *type;
|
|
};
|
|
// clang-format on
|
|
|
|
// clang-format off
|
|
static PyTypeObject PyCypherTypeType = {
|
|
PyVarObject_HEAD_INIT(nullptr, 0)
|
|
.tp_name = "_mgp.Type",
|
|
.tp_basicsize = sizeof(PyCypherType),
|
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
|
.tp_doc = "Wraps struct mgp_type.",
|
|
};
|
|
// clang-format on
|
|
|
|
PyObject *MakePyCypherType(mgp_type *type) {
|
|
MG_ASSERT(type);
|
|
auto *py_type = PyObject_New(PyCypherType, &PyCypherTypeType);
|
|
if (!py_type) return nullptr;
|
|
py_type->type = type;
|
|
return reinterpret_cast<PyObject *>(py_type);
|
|
}
|
|
|
|
// clang-format off
|
|
struct PyQueryProc {
|
|
PyObject_HEAD
|
|
mgp_proc *callable;
|
|
};
|
|
// clang-format on
|
|
|
|
// clang-format off
|
|
struct PyMagicFunc{
|
|
PyObject_HEAD
|
|
mgp_func *callable;
|
|
};
|
|
// clang-format on
|
|
|
|
template <typename T>
|
|
concept IsCallable = utils::SameAsAnyOf<T, PyQueryProc, PyMagicFunc>;
|
|
|
|
template <IsCallable TCall>
|
|
PyObject *PyCallableAddArg(TCall *self, PyObject *args) {
|
|
MG_ASSERT(self->callable);
|
|
const char *name = nullptr;
|
|
PyCypherType *py_type = nullptr;
|
|
if (!PyArg_ParseTuple(args, "sO!", &name, &PyCypherTypeType, &py_type)) return nullptr;
|
|
auto *type = py_type->type;
|
|
|
|
if constexpr (std::is_same_v<TCall, PyQueryProc>) {
|
|
if (RaiseExceptionFromErrorCode(mgp_proc_add_arg(self->callable, name, type))) {
|
|
return nullptr;
|
|
}
|
|
} else if constexpr (std::is_same_v<TCall, PyMagicFunc>) {
|
|
if (RaiseExceptionFromErrorCode(mgp_func_add_arg(self->callable, name, type))) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
template <IsCallable TCall>
|
|
PyObject *PyCallableAddOptArg(TCall *self, PyObject *args) {
|
|
MG_ASSERT(self->callable);
|
|
const char *name = nullptr;
|
|
PyCypherType *py_type = nullptr;
|
|
PyObject *py_value = nullptr;
|
|
if (!PyArg_ParseTuple(args, "sO!O", &name, &PyCypherTypeType, &py_type, &py_value)) return nullptr;
|
|
auto *type = py_type->type;
|
|
mgp_memory memory{self->callable->opt_args.get_allocator().GetMemoryResource()};
|
|
mgp_value *value = PyObjectToMgpValueWithPythonExceptions(py_value, &memory);
|
|
if (value == nullptr) {
|
|
return nullptr;
|
|
}
|
|
if constexpr (std::is_same_v<TCall, PyQueryProc>) {
|
|
if (RaiseExceptionFromErrorCode(mgp_proc_add_opt_arg(self->callable, name, type, value))) {
|
|
mgp_value_destroy(value);
|
|
return nullptr;
|
|
}
|
|
} else if constexpr (std::is_same_v<TCall, PyMagicFunc>) {
|
|
if (RaiseExceptionFromErrorCode(mgp_func_add_opt_arg(self->callable, name, type, value))) {
|
|
mgp_value_destroy(value);
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
mgp_value_destroy(value);
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
PyObject *PyQueryProcAddArg(PyQueryProc *self, PyObject *args) { return PyCallableAddArg(self, args); }
|
|
|
|
PyObject *PyQueryProcAddOptArg(PyQueryProc *self, PyObject *args) { return PyCallableAddOptArg(self, args); }
|
|
|
|
PyObject *PyQueryProcAddResult(PyQueryProc *self, PyObject *args) {
|
|
MG_ASSERT(self->callable);
|
|
const char *name = nullptr;
|
|
PyCypherType *py_type = nullptr;
|
|
if (!PyArg_ParseTuple(args, "sO!", &name, &PyCypherTypeType, &py_type)) return nullptr;
|
|
|
|
auto *type = reinterpret_cast<PyCypherType *>(py_type)->type;
|
|
if (RaiseExceptionFromErrorCode(mgp_proc_add_result(self->callable, name, type))) {
|
|
return nullptr;
|
|
}
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
PyObject *PyQueryProcAddDeprecatedResult(PyQueryProc *self, PyObject *args) {
|
|
MG_ASSERT(self->callable);
|
|
const char *name = nullptr;
|
|
PyCypherType *py_type = nullptr;
|
|
if (!PyArg_ParseTuple(args, "sO!", &name, &PyCypherTypeType, &py_type)) return nullptr;
|
|
auto *type = reinterpret_cast<PyCypherType *>(py_type)->type;
|
|
if (RaiseExceptionFromErrorCode(mgp_proc_add_deprecated_result(self->callable, name, type))) {
|
|
return nullptr;
|
|
}
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
static PyMethodDef PyQueryProcMethods[] = {
|
|
{"__reduce__", reinterpret_cast<PyCFunction>(DisallowPickleAndCopy), METH_NOARGS, "__reduce__ is not supported"},
|
|
{"add_arg", reinterpret_cast<PyCFunction>(PyQueryProcAddArg), METH_VARARGS,
|
|
"Add a required argument to a procedure."},
|
|
{"add_opt_arg", reinterpret_cast<PyCFunction>(PyQueryProcAddOptArg), METH_VARARGS,
|
|
"Add an optional argument with a default value to a procedure."},
|
|
{"add_result", reinterpret_cast<PyCFunction>(PyQueryProcAddResult), METH_VARARGS,
|
|
"Add a result field to a procedure."},
|
|
{"add_deprecated_result", reinterpret_cast<PyCFunction>(PyQueryProcAddDeprecatedResult), METH_VARARGS,
|
|
"Add a result field to a procedure and mark it as deprecated."},
|
|
{nullptr},
|
|
};
|
|
|
|
// clang-format off
|
|
static PyTypeObject PyQueryProcType = {
|
|
PyVarObject_HEAD_INIT(nullptr, 0)
|
|
.tp_name = "_mgp.Proc",
|
|
.tp_basicsize = sizeof(PyQueryProc),
|
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
|
.tp_doc = "Wraps struct mgp_proc.",
|
|
.tp_methods = PyQueryProcMethods,
|
|
};
|
|
// clang-format on
|
|
|
|
PyObject *PyMagicFuncAddArg(PyMagicFunc *self, PyObject *args) { return PyCallableAddArg(self, args); }
|
|
|
|
PyObject *PyMagicFuncAddOptArg(PyMagicFunc *self, PyObject *args) { return PyCallableAddOptArg(self, args); }
|
|
|
|
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
|
static PyMethodDef PyMagicFuncMethods[] = {
|
|
{"__reduce__", reinterpret_cast<PyCFunction>(DisallowPickleAndCopy), METH_NOARGS, "__reduce__ is not supported"},
|
|
{"add_arg", reinterpret_cast<PyCFunction>(PyMagicFuncAddArg), METH_VARARGS,
|
|
"Add a required argument to a function."},
|
|
{"add_opt_arg", reinterpret_cast<PyCFunction>(PyMagicFuncAddOptArg), METH_VARARGS,
|
|
"Add an optional argument with a default value to a function."},
|
|
{nullptr},
|
|
};
|
|
|
|
// clang-format off
|
|
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
|
static PyTypeObject PyMagicFuncType = {
|
|
PyVarObject_HEAD_INIT(nullptr, 0)
|
|
.tp_name = "_mgp.Func",
|
|
.tp_basicsize = sizeof(PyMagicFunc),
|
|
// NOLINTNEXTLINE(hicpp-signed-bitwise)
|
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
|
.tp_doc = "Wraps struct mgp_func.",
|
|
.tp_methods = PyMagicFuncMethods,
|
|
};
|
|
// clang-format on
|
|
|
|
// clang-format off
|
|
struct PyQueryModule {
|
|
PyObject_HEAD
|
|
mgp_module *module;
|
|
};
|
|
// clang-format on
|
|
|
|
struct PyMessages {
|
|
PyObject_HEAD;
|
|
mgp_messages *messages;
|
|
mgp_memory *memory;
|
|
};
|
|
|
|
struct PyMessage {
|
|
PyObject_HEAD;
|
|
mgp_message *message;
|
|
const PyMessages *messages;
|
|
mgp_memory *memory;
|
|
};
|
|
|
|
PyObject *PyMessagesIsValid(const PyMessages *self, PyObject *Py_UNUSED(ignored)) {
|
|
return PyBool_FromLong(!!self->messages);
|
|
}
|
|
|
|
PyObject *PyMessageIsValid(PyMessage *self, PyObject *Py_UNUSED(ignored)) {
|
|
return PyMessagesIsValid(self->messages, nullptr);
|
|
}
|
|
|
|
PyObject *PyMessageGetSourceType(PyMessage *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(self->message);
|
|
MG_ASSERT(self->memory);
|
|
mgp_source_type source_type{mgp_source_type::KAFKA};
|
|
if (RaiseExceptionFromErrorCode(mgp_message_source_type(self->message, &source_type))) {
|
|
return nullptr;
|
|
}
|
|
auto *py_source_type = PyLong_FromLong(static_cast<int64_t>(source_type));
|
|
if (!py_source_type) {
|
|
PyErr_SetString(PyExc_RuntimeError, "Unable to get long from source type");
|
|
return nullptr;
|
|
}
|
|
return py_source_type;
|
|
}
|
|
|
|
PyObject *PyMessageGetPayload(PyMessage *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(self->message);
|
|
size_t payload_size{0};
|
|
if (RaiseExceptionFromErrorCode(mgp_message_payload_size(self->message, &payload_size))) {
|
|
return nullptr;
|
|
}
|
|
const char *payload{nullptr};
|
|
if (RaiseExceptionFromErrorCode(mgp_message_payload(self->message, &payload))) {
|
|
return nullptr;
|
|
}
|
|
auto *raw_bytes = PyByteArray_FromStringAndSize(payload, payload_size);
|
|
if (!raw_bytes) {
|
|
PyErr_SetString(PyExc_RuntimeError, "Unable to get raw bytes from payload");
|
|
return nullptr;
|
|
}
|
|
return raw_bytes;
|
|
}
|
|
|
|
PyObject *PyMessageGetTopicName(PyMessage *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(self->message);
|
|
MG_ASSERT(self->memory);
|
|
const char *topic_name{nullptr};
|
|
if (RaiseExceptionFromErrorCode(mgp_message_topic_name(self->message, &topic_name))) {
|
|
return nullptr;
|
|
}
|
|
auto *py_topic_name = PyUnicode_FromString(topic_name);
|
|
if (!py_topic_name) {
|
|
PyErr_SetString(PyExc_RuntimeError, "Unable to get string from topic_name");
|
|
return nullptr;
|
|
}
|
|
return py_topic_name;
|
|
}
|
|
|
|
PyObject *PyMessageGetKey(PyMessage *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(self->message);
|
|
MG_ASSERT(self->memory);
|
|
size_t key_size{0};
|
|
if (RaiseExceptionFromErrorCode(mgp_message_key_size(self->message, &key_size))) {
|
|
return nullptr;
|
|
}
|
|
const char *key{nullptr};
|
|
if (RaiseExceptionFromErrorCode(mgp_message_key(self->message, &key))) {
|
|
return nullptr;
|
|
}
|
|
auto *raw_bytes = PyByteArray_FromStringAndSize(key, key_size);
|
|
if (!raw_bytes) {
|
|
PyErr_SetString(PyExc_RuntimeError, "Unable to get raw bytes from payload");
|
|
return nullptr;
|
|
}
|
|
return raw_bytes;
|
|
}
|
|
|
|
PyObject *PyMessageGetTimestamp(PyMessage *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(self->message);
|
|
MG_ASSERT(self->memory);
|
|
int64_t timestamp{0};
|
|
if (RaiseExceptionFromErrorCode(mgp_message_timestamp(self->message, ×tamp))) {
|
|
return nullptr;
|
|
}
|
|
auto *py_int = PyLong_FromUnsignedLong(timestamp);
|
|
if (!py_int) {
|
|
PyErr_SetString(PyExc_IndexError, "Unable to get timestamp.");
|
|
return nullptr;
|
|
}
|
|
return py_int;
|
|
}
|
|
|
|
PyObject *PyMessageGetOffset(PyMessage *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(self->message);
|
|
MG_ASSERT(self->memory);
|
|
int64_t offset{0};
|
|
if (RaiseExceptionFromErrorCode(mgp_message_offset(self->message, &offset))) {
|
|
return nullptr;
|
|
}
|
|
auto *py_int = PyLong_FromLongLong(offset);
|
|
if (!py_int) {
|
|
PyErr_SetString(PyExc_IndexError, "Unable to get offset");
|
|
return nullptr;
|
|
}
|
|
return py_int;
|
|
}
|
|
|
|
// NOLINTNEXTLINE
|
|
static PyMethodDef PyMessageMethods[] = {
|
|
{"__reduce__", reinterpret_cast<PyCFunction>(DisallowPickleAndCopy), METH_NOARGS, "__reduce__ is not supported"},
|
|
{"is_valid", reinterpret_cast<PyCFunction>(PyMessageIsValid), METH_NOARGS,
|
|
"Return True if messages is in valid context and may be used."},
|
|
{"source_type", reinterpret_cast<PyCFunction>(PyMessageGetSourceType), METH_NOARGS, "Get stream source type."},
|
|
{"payload", reinterpret_cast<PyCFunction>(PyMessageGetPayload), METH_NOARGS, "Get payload"},
|
|
{"topic_name", reinterpret_cast<PyCFunction>(PyMessageGetTopicName), METH_NOARGS, "Get topic name."},
|
|
{"key", reinterpret_cast<PyCFunction>(PyMessageGetKey), METH_NOARGS, "Get message key."},
|
|
{"timestamp", reinterpret_cast<PyCFunction>(PyMessageGetTimestamp), METH_NOARGS, "Get message timestamp."},
|
|
{"offset", reinterpret_cast<PyCFunction>(PyMessageGetOffset), METH_NOARGS, "Get message offset."},
|
|
{nullptr},
|
|
};
|
|
|
|
void PyMessageDealloc(PyMessage *self) {
|
|
MG_ASSERT(self->memory);
|
|
MG_ASSERT(self->message);
|
|
MG_ASSERT(self->messages);
|
|
// NOLINTNEXTLINE
|
|
Py_DECREF(self->messages);
|
|
// NOLINTNEXTLINE
|
|
Py_TYPE(self)->tp_free(self);
|
|
}
|
|
|
|
// NOLINTNEXTLINE
|
|
static PyTypeObject PyMessageType = {
|
|
PyVarObject_HEAD_INIT(nullptr, 0).tp_name = "_mgp.Message",
|
|
.tp_basicsize = sizeof(PyMessage),
|
|
.tp_dealloc = reinterpret_cast<destructor>(PyMessageDealloc),
|
|
// NOLINTNEXTLINE
|
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
|
.tp_doc = "Wraps struct mgp_message.",
|
|
// NOLINTNEXTLINE
|
|
.tp_methods = PyMessageMethods,
|
|
};
|
|
|
|
PyObject *PyMessagesInvalidate(PyMessages *self, PyObject *Py_UNUSED(ignored)) {
|
|
self->messages = nullptr;
|
|
self->memory = nullptr;
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
PyObject *PyMessagesGetTotalMessages(PyMessages *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(self->messages);
|
|
MG_ASSERT(self->memory);
|
|
auto size = self->messages->messages.size();
|
|
auto *py_int = PyLong_FromSize_t(size);
|
|
if (!py_int) {
|
|
PyErr_SetString(PyExc_IndexError, "Unable to get total messages count.");
|
|
return nullptr;
|
|
}
|
|
return py_int;
|
|
}
|
|
|
|
PyObject *PyMessagesGetMessageAt(PyMessages *self, PyObject *args) {
|
|
MG_ASSERT(self->messages);
|
|
MG_ASSERT(self->memory);
|
|
int64_t id = 0;
|
|
if (!PyArg_ParseTuple(args, "l", &id)) return nullptr;
|
|
if (id < 0 || id >= self->messages->messages.size()) return nullptr;
|
|
auto *message = &self->messages->messages[id];
|
|
// NOLINTNEXTLINE
|
|
auto *py_message = PyObject_New(PyMessage, &PyMessageType);
|
|
if (!py_message) {
|
|
return nullptr;
|
|
}
|
|
py_message->message = message;
|
|
// NOLINTNEXTLINE
|
|
Py_INCREF(self);
|
|
py_message->messages = self;
|
|
py_message->memory = self->memory;
|
|
if (!message) {
|
|
PyErr_SetString(PyExc_IndexError, "Unable to find the message with given index.");
|
|
return nullptr;
|
|
}
|
|
// NOLINTNEXTLINE
|
|
return reinterpret_cast<PyObject *>(py_message);
|
|
}
|
|
|
|
// NOLINTNEXTLINE
|
|
static PyMethodDef PyMessagesMethods[] = {
|
|
{"__reduce__", reinterpret_cast<PyCFunction>(DisallowPickleAndCopy), METH_NOARGS, "__reduce__ is not supported"},
|
|
{"invalidate", reinterpret_cast<PyCFunction>(PyMessagesInvalidate), METH_NOARGS,
|
|
"Invalidate the messages context thus preventing the messages from being used"},
|
|
{"is_valid", reinterpret_cast<PyCFunction>(PyMessagesIsValid), METH_NOARGS,
|
|
"Return True if messages is in valid context and may be used."},
|
|
{"total_messages", reinterpret_cast<PyCFunction>(PyMessagesGetTotalMessages), METH_VARARGS,
|
|
"Get number of messages available"},
|
|
{"message_at", reinterpret_cast<PyCFunction>(PyMessagesGetMessageAt), METH_VARARGS,
|
|
"Get message at index idx from messages"},
|
|
{nullptr},
|
|
};
|
|
|
|
// NOLINTNEXTLINE
|
|
static PyTypeObject PyMessagesType = {
|
|
PyVarObject_HEAD_INIT(nullptr, 0).tp_name = "_mgp.Messages",
|
|
.tp_basicsize = sizeof(PyMessages),
|
|
// NOLINTNEXTLINE
|
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
|
.tp_doc = "Wraps struct mgp_messages.",
|
|
// NOLINTNEXTLINE
|
|
.tp_methods = PyMessagesMethods,
|
|
};
|
|
|
|
PyObject *MakePyMessages(mgp_messages *msgs, mgp_memory *memory) {
|
|
MG_ASSERT(!msgs || (msgs && memory));
|
|
// NOLINTNEXTLINE
|
|
auto *py_messages = PyObject_New(PyMessages, &PyMessagesType);
|
|
if (!py_messages) return nullptr;
|
|
py_messages->messages = msgs;
|
|
py_messages->memory = memory;
|
|
return reinterpret_cast<PyObject *>(py_messages);
|
|
}
|
|
|
|
py::Object MgpListToPyTuple(mgp_list *list, PyGraph *py_graph) {
|
|
MG_ASSERT(list);
|
|
MG_ASSERT(py_graph);
|
|
const auto len = list->elems.size();
|
|
py::Object py_tuple(PyTuple_New(len));
|
|
if (!py_tuple) return nullptr;
|
|
for (size_t i = 0; i < len; ++i) {
|
|
auto elem = MgpValueToPyObject(list->elems[i], py_graph);
|
|
if (!elem) return nullptr;
|
|
// Explicitly convert `py_tuple`, which is `py::Object`, via static_cast.
|
|
// Then the macro will cast it to `PyTuple *`.
|
|
PyTuple_SET_ITEM(py_tuple.Ptr(), i, elem.Steal());
|
|
}
|
|
return py_tuple;
|
|
}
|
|
|
|
py::Object MgpListToPyTuple(mgp_list *list, PyObject *py_graph) {
|
|
if (Py_TYPE(py_graph) != &PyGraphType) {
|
|
PyErr_SetString(PyExc_TypeError, "Expected a _mgp.Graph.");
|
|
return nullptr;
|
|
}
|
|
return MgpListToPyTuple(list, reinterpret_cast<PyGraph *>(py_graph));
|
|
}
|
|
|
|
namespace {
|
|
std::optional<py::ExceptionInfo> AddRecordFromPython(mgp_result *result, py::Object py_record) {
|
|
py::Object py_mgp(PyImport_ImportModule("mgp"));
|
|
if (!py_mgp) return py::FetchError();
|
|
auto record_cls = py_mgp.GetAttr("Record");
|
|
if (!record_cls) return py::FetchError();
|
|
if (!PyObject_IsInstance(py_record.Ptr(), record_cls.Ptr())) {
|
|
std::stringstream ss;
|
|
ss << "Value '" << py_record << "' is not an instance of 'mgp.Record'";
|
|
const auto &msg = ss.str();
|
|
PyErr_SetString(PyExc_TypeError, msg.c_str());
|
|
return py::FetchError();
|
|
}
|
|
py::Object fields(py_record.GetAttr("fields"));
|
|
if (!fields) return py::FetchError();
|
|
if (!PyDict_Check(fields)) {
|
|
PyErr_SetString(PyExc_TypeError, "Expected 'mgp.Record.fields' to be a 'dict'");
|
|
return py::FetchError();
|
|
}
|
|
py::Object items(PyDict_Items(fields.Ptr()));
|
|
if (!items) return py::FetchError();
|
|
mgp_result_record *record{nullptr};
|
|
if (RaiseExceptionFromErrorCode(mgp_result_new_record(result, &record))) {
|
|
return py::FetchError();
|
|
}
|
|
Py_ssize_t len = PyList_GET_SIZE(items.Ptr());
|
|
for (Py_ssize_t i = 0; i < len; ++i) {
|
|
auto *item = PyList_GET_ITEM(items.Ptr(), i);
|
|
if (!item) return py::FetchError();
|
|
MG_ASSERT(PyTuple_Check(item));
|
|
auto *key = PyTuple_GetItem(item, 0);
|
|
if (!key) return py::FetchError();
|
|
if (!PyUnicode_Check(key)) {
|
|
std::stringstream ss;
|
|
ss << "Field name '" << py::Object::FromBorrow(key) << "' is not an instance of 'str'";
|
|
const auto &msg = ss.str();
|
|
PyErr_SetString(PyExc_TypeError, msg.c_str());
|
|
return py::FetchError();
|
|
}
|
|
const auto *field_name = PyUnicode_AsUTF8(key);
|
|
if (!field_name) return py::FetchError();
|
|
auto *val = PyTuple_GetItem(item, 1);
|
|
if (!val) return py::FetchError();
|
|
mgp_memory memory{result->rows.get_allocator().GetMemoryResource()};
|
|
mgp_value *field_val = PyObjectToMgpValueWithPythonExceptions(val, &memory);
|
|
if (field_val == nullptr) {
|
|
return py::FetchError();
|
|
}
|
|
if (mgp_result_record_insert(record, field_name, field_val) != mgp_error::MGP_ERROR_NO_ERROR) {
|
|
std::stringstream ss;
|
|
ss << "Unable to insert field '" << py::Object::FromBorrow(key) << "' with value: '"
|
|
<< py::Object::FromBorrow(val) << "'; did you set the correct field type?";
|
|
const auto &msg = ss.str();
|
|
PyErr_SetString(PyExc_ValueError, msg.c_str());
|
|
mgp_value_destroy(field_val);
|
|
return py::FetchError();
|
|
}
|
|
mgp_value_destroy(field_val);
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::optional<py::ExceptionInfo> AddMultipleRecordsFromPython(mgp_result *result, py::Object py_seq) {
|
|
Py_ssize_t len = PySequence_Size(py_seq.Ptr());
|
|
if (len == -1) return py::FetchError();
|
|
for (Py_ssize_t i = 0; i < len; ++i) {
|
|
py::Object py_record(PySequence_GetItem(py_seq.Ptr(), i));
|
|
if (!py_record) return py::FetchError();
|
|
auto maybe_exc = AddRecordFromPython(result, py_record);
|
|
if (maybe_exc) return maybe_exc;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::function<void()> PyObjectCleanup(py::Object &py_object) {
|
|
return [py_object]() {
|
|
// Run `gc.collect` (reference cycle-detection) explicitly, so that we are
|
|
// sure the procedure cleaned up everything it held references to. If the
|
|
// user stored a reference to one of our `_mgp` instances then the
|
|
// internally used `mgp_*` structs will stay unfreed and a memory leak
|
|
// will be reported at the end of the query execution.
|
|
py::Object gc(PyImport_ImportModule("gc"));
|
|
if (!gc) {
|
|
LOG_FATAL(py::FetchError().value());
|
|
}
|
|
|
|
if (!gc.CallMethod("collect")) {
|
|
LOG_FATAL(py::FetchError().value());
|
|
}
|
|
|
|
// After making sure all references from our side have been cleared,
|
|
// invalidate the `_mgp.Graph` object. If the user kept a reference to one
|
|
// of our `_mgp` instances then this will prevent them from using those
|
|
// objects (whose internal `mgp_*` pointers are now invalid and would cause
|
|
// a crash).
|
|
if (!py_object.CallMethod("invalidate")) {
|
|
LOG_FATAL(py::FetchError().value());
|
|
}
|
|
};
|
|
}
|
|
|
|
void CallPythonProcedure(const py::Object &py_cb, mgp_list *args, mgp_graph *graph, mgp_result *result,
|
|
mgp_memory *memory) {
|
|
auto gil = py::EnsureGIL();
|
|
|
|
auto error_to_msg = [](const std::optional<py::ExceptionInfo> &exc_info) -> std::optional<std::string> {
|
|
if (!exc_info) return std::nullopt;
|
|
// Here we tell the traceback formatter to skip the first line of the
|
|
// traceback because that line will always be our wrapper function in our
|
|
// internal `mgp.py` file. With that line skipped, the user will always
|
|
// get only the relevant traceback that happened in his Python code.
|
|
return py::FormatException(*exc_info, /* skip_first_line = */ true);
|
|
};
|
|
|
|
auto call = [&](py::Object py_graph) -> std::optional<py::ExceptionInfo> {
|
|
py::Object py_args(MgpListToPyTuple(args, py_graph.Ptr()));
|
|
if (!py_args) return py::FetchError();
|
|
auto py_res = py_cb.Call(py_graph, py_args);
|
|
if (!py_res) return py::FetchError();
|
|
if (PySequence_Check(py_res.Ptr())) {
|
|
return AddMultipleRecordsFromPython(result, py_res);
|
|
} else {
|
|
return AddRecordFromPython(result, py_res);
|
|
}
|
|
};
|
|
|
|
// It is *VERY IMPORTANT* to note that this code takes great care not to keep
|
|
// any extra references to any `_mgp` instances (except for `_mgp.Graph`), so
|
|
// as not to introduce extra reference counts and prevent their deallocation.
|
|
// In particular, the `ExceptionInfo` object has a `traceback` field that
|
|
// contains references to the Python frames and their arguments, and therefore
|
|
// our `_mgp` instances as well. Within this code we ensure not to keep the
|
|
// `ExceptionInfo` object alive so that no extra reference counts are
|
|
// introduced. We only fetch the error message and immediately destroy the
|
|
// object.
|
|
std::optional<std::string> maybe_msg;
|
|
{
|
|
py::Object py_graph(MakePyGraph(graph, memory));
|
|
utils::OnScopeExit clean_up(PyObjectCleanup(py_graph));
|
|
if (py_graph) {
|
|
maybe_msg = error_to_msg(call(py_graph));
|
|
} else {
|
|
maybe_msg = error_to_msg(py::FetchError());
|
|
}
|
|
}
|
|
|
|
if (maybe_msg) {
|
|
static_cast<void>(mgp_result_set_error_msg(result, maybe_msg->c_str()));
|
|
}
|
|
}
|
|
|
|
void CallPythonTransformation(const py::Object &py_cb, mgp_messages *msgs, mgp_graph *graph, mgp_result *result,
|
|
mgp_memory *memory) {
|
|
auto gil = py::EnsureGIL();
|
|
|
|
auto error_to_msg = [](const std::optional<py::ExceptionInfo> &exc_info) -> std::optional<std::string> {
|
|
if (!exc_info) return std::nullopt;
|
|
// Here we tell the traceback formatter to skip the first line of the
|
|
// traceback because that line will always be our wrapper function in our
|
|
// internal `mgp.py` file. With that line skipped, the user will always
|
|
// get only the relevant traceback that happened in his Python code.
|
|
return py::FormatException(*exc_info, /* skip_first_line = */ true);
|
|
};
|
|
|
|
auto call = [&](py::Object py_graph, py::Object py_messages) -> std::optional<py::ExceptionInfo> {
|
|
auto py_res = py_cb.Call(py_graph, py_messages);
|
|
if (!py_res) return py::FetchError();
|
|
if (PySequence_Check(py_res.Ptr())) {
|
|
return AddMultipleRecordsFromPython(result, py_res);
|
|
}
|
|
return AddRecordFromPython(result, py_res);
|
|
};
|
|
|
|
// It is *VERY IMPORTANT* to note that this code takes great care not to keep
|
|
// any extra references to any `_mgp` instances (except for `_mgp.Graph`), so
|
|
// as not to introduce extra reference counts and prevent their deallocation.
|
|
// In particular, the `ExceptionInfo` object has a `traceback` field that
|
|
// contains references to the Python frames and their arguments, and therefore
|
|
// our `_mgp` instances as well. Within this code we ensure not to keep the
|
|
// `ExceptionInfo` object alive so that no extra reference counts are
|
|
// introduced. We only fetch the error message and immediately destroy the
|
|
// object.
|
|
std::optional<std::string> maybe_msg;
|
|
{
|
|
py::Object py_graph(MakePyGraph(graph, memory));
|
|
py::Object py_messages(MakePyMessages(msgs, memory));
|
|
|
|
utils::OnScopeExit clean_up_graph(PyObjectCleanup(py_graph));
|
|
utils::OnScopeExit clean_up_messages(PyObjectCleanup(py_messages));
|
|
|
|
if (py_graph && py_messages) {
|
|
maybe_msg = error_to_msg(call(py_graph, py_messages));
|
|
} else {
|
|
maybe_msg = error_to_msg(py::FetchError());
|
|
}
|
|
}
|
|
|
|
if (maybe_msg) {
|
|
static_cast<void>(mgp_result_set_error_msg(result, maybe_msg->c_str()));
|
|
}
|
|
}
|
|
|
|
void CallPythonFunction(const py::Object &py_cb, mgp_list *args, mgp_graph *graph, mgp_func_result *result,
|
|
mgp_memory *memory) {
|
|
auto gil = py::EnsureGIL();
|
|
|
|
auto error_to_msg = [](const std::optional<py::ExceptionInfo> &exc_info) -> std::optional<std::string> {
|
|
if (!exc_info) return std::nullopt;
|
|
// Here we tell the traceback formatter to skip the first line of the
|
|
// traceback because that line will always be our wrapper function in our
|
|
// internal `mgp.py` file. With that line skipped, the user will always
|
|
// get only the relevant traceback that happened in his Python code.
|
|
return py::FormatException(*exc_info, /* skip_first_line = */ true);
|
|
};
|
|
|
|
auto call = [&](py::Object py_graph) -> utils::BasicResult<std::optional<py::ExceptionInfo>, mgp_value *> {
|
|
py::Object py_args(MgpListToPyTuple(args, py_graph.Ptr()));
|
|
if (!py_args) return {py::FetchError()};
|
|
auto py_res = py_cb.Call(py_graph, py_args);
|
|
if (!py_res) return {py::FetchError()};
|
|
mgp_value *ret_val = PyObjectToMgpValueWithPythonExceptions(py_res.Ptr(), memory);
|
|
if (ret_val == nullptr) {
|
|
return {py::FetchError()};
|
|
}
|
|
return ret_val;
|
|
};
|
|
|
|
// It is *VERY IMPORTANT* to note that this code takes great care not to keep
|
|
// any extra references to any `_mgp` instances (except for `_mgp.Graph`), so
|
|
// as not to introduce extra reference counts and prevent their deallocation.
|
|
// In particular, the `ExceptionInfo` object has a `traceback` field that
|
|
// contains references to the Python frames and their arguments, and therefore
|
|
// our `_mgp` instances as well. Within this code we ensure not to keep the
|
|
// `ExceptionInfo` object alive so that no extra reference counts are
|
|
// introduced. We only fetch the error message and immediately destroy the
|
|
// object.
|
|
std::optional<std::string> maybe_msg;
|
|
{
|
|
py::Object py_graph(MakePyGraph(graph, memory));
|
|
utils::OnScopeExit clean_up(PyObjectCleanup(py_graph));
|
|
if (py_graph) {
|
|
auto maybe_result = call(py_graph);
|
|
if (!maybe_result.HasError()) {
|
|
static_cast<void>(mgp_func_result_set_value(result, maybe_result.GetValue(), memory));
|
|
return;
|
|
}
|
|
maybe_msg = error_to_msg(maybe_result.GetError());
|
|
} else {
|
|
maybe_msg = error_to_msg(py::FetchError());
|
|
}
|
|
}
|
|
|
|
if (maybe_msg) {
|
|
static_cast<void>(
|
|
mgp_func_result_set_error_msg(result, maybe_msg->c_str(), memory)); // No error fetching if this fails
|
|
}
|
|
}
|
|
|
|
PyObject *PyQueryModuleAddProcedure(PyQueryModule *self, PyObject *cb, bool is_write_procedure) {
|
|
MG_ASSERT(self->module);
|
|
if (!PyCallable_Check(cb)) {
|
|
PyErr_SetString(PyExc_TypeError, "Expected a callable object.");
|
|
return nullptr;
|
|
}
|
|
auto py_cb = py::Object::FromBorrow(cb);
|
|
py::Object py_name(py_cb.GetAttr("__name__"));
|
|
const auto *name = PyUnicode_AsUTF8(py_name.Ptr());
|
|
if (!name) return nullptr;
|
|
if (!IsValidIdentifierName(name)) {
|
|
PyErr_SetString(PyExc_ValueError, "Procedure name is not a valid identifier");
|
|
return nullptr;
|
|
}
|
|
auto *memory = self->module->procedures.get_allocator().GetMemoryResource();
|
|
mgp_proc proc(name,
|
|
[py_cb](mgp_list *args, mgp_graph *graph, mgp_result *result, mgp_memory *memory) {
|
|
CallPythonProcedure(py_cb, args, graph, result, memory);
|
|
},
|
|
memory, {.is_write = is_write_procedure});
|
|
const auto &[proc_it, did_insert] = self->module->procedures.emplace(name, std::move(proc));
|
|
if (!did_insert) {
|
|
PyErr_SetString(PyExc_ValueError, "Already registered a procedure with the same name.");
|
|
return nullptr;
|
|
}
|
|
auto *py_proc = PyObject_New(PyQueryProc, &PyQueryProcType); // NOLINT(cppcoreguidelines-pro-type-cstyle-cast)
|
|
if (!py_proc) return nullptr;
|
|
py_proc->callable = &proc_it->second;
|
|
return reinterpret_cast<PyObject *>(py_proc);
|
|
}
|
|
} // namespace
|
|
|
|
PyObject *PyQueryModuleAddReadProcedure(PyQueryModule *self, PyObject *cb) {
|
|
return PyQueryModuleAddProcedure(self, cb, false);
|
|
}
|
|
|
|
PyObject *PyQueryModuleAddWriteProcedure(PyQueryModule *self, PyObject *cb) {
|
|
return PyQueryModuleAddProcedure(self, cb, true);
|
|
}
|
|
|
|
PyObject *PyQueryModuleAddTransformation(PyQueryModule *self, PyObject *cb) {
|
|
MG_ASSERT(self->module);
|
|
if (!PyCallable_Check(cb)) {
|
|
PyErr_SetString(PyExc_TypeError, "Expected a callable object.");
|
|
return nullptr;
|
|
}
|
|
auto py_cb = py::Object::FromBorrow(cb);
|
|
py::Object py_name(py_cb.GetAttr("__name__"));
|
|
const auto *name = PyUnicode_AsUTF8(py_name.Ptr());
|
|
if (!name) return nullptr;
|
|
if (!IsValidIdentifierName(name)) {
|
|
PyErr_SetString(PyExc_ValueError, "Transformation name is not a valid identifier");
|
|
return nullptr;
|
|
}
|
|
auto *memory = self->module->transformations.get_allocator().GetMemoryResource();
|
|
mgp_trans trans(
|
|
name,
|
|
[py_cb](mgp_messages *msgs, mgp_graph *graph, mgp_result *result, mgp_memory *memory) {
|
|
CallPythonTransformation(py_cb, msgs, graph, result, memory);
|
|
},
|
|
memory);
|
|
const auto [trans_it, did_insert] = self->module->transformations.emplace(name, std::move(trans));
|
|
if (!did_insert) {
|
|
PyErr_SetString(PyExc_ValueError, "Already registered a procedure with the same name.");
|
|
return nullptr;
|
|
}
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
PyObject *PyQueryModuleAddFunction(PyQueryModule *self, PyObject *cb) {
|
|
MG_ASSERT(self->module);
|
|
if (!PyCallable_Check(cb)) {
|
|
PyErr_SetString(PyExc_TypeError, "Expected a callable object.");
|
|
return nullptr;
|
|
}
|
|
auto py_cb = py::Object::FromBorrow(cb);
|
|
py::Object py_name(py_cb.GetAttr("__name__"));
|
|
const auto *name = PyUnicode_AsUTF8(py_name.Ptr());
|
|
if (!name) return nullptr;
|
|
if (!IsValidIdentifierName(name)) {
|
|
PyErr_SetString(PyExc_ValueError, "Function name is not a valid identifier");
|
|
return nullptr;
|
|
}
|
|
auto *memory = self->module->functions.get_allocator().GetMemoryResource();
|
|
mgp_func func(
|
|
name,
|
|
[py_cb](mgp_list *args, mgp_func_context *func_ctx, mgp_func_result *result, mgp_memory *memory) {
|
|
auto graph = mgp_graph::NonWritableGraph(*(func_ctx->impl), func_ctx->view);
|
|
return CallPythonFunction(py_cb, args, &graph, result, memory);
|
|
},
|
|
memory);
|
|
const auto [func_it, did_insert] = self->module->functions.emplace(name, std::move(func));
|
|
if (!did_insert) {
|
|
PyErr_SetString(PyExc_ValueError, "Already registered a function with the same name.");
|
|
return nullptr;
|
|
}
|
|
auto *py_func = PyObject_New(PyMagicFunc, &PyMagicFuncType); // NOLINT(cppcoreguidelines-pro-type-cstyle-cast)
|
|
if (!py_func) return nullptr;
|
|
py_func->callable = &func_it->second;
|
|
return reinterpret_cast<PyObject *>(py_func);
|
|
}
|
|
|
|
static PyMethodDef PyQueryModuleMethods[] = {
|
|
{"__reduce__", reinterpret_cast<PyCFunction>(DisallowPickleAndCopy), METH_NOARGS, "__reduce__ is not supported"},
|
|
{"add_read_procedure", reinterpret_cast<PyCFunction>(PyQueryModuleAddReadProcedure), METH_O,
|
|
"Register a read-only procedure with this module."},
|
|
{"add_write_procedure", reinterpret_cast<PyCFunction>(PyQueryModuleAddWriteProcedure), METH_O,
|
|
"Register a writeable procedure with this module."},
|
|
{"add_transformation", reinterpret_cast<PyCFunction>(PyQueryModuleAddTransformation), METH_O,
|
|
"Register a transformation with this module."},
|
|
{"add_function", reinterpret_cast<PyCFunction>(PyQueryModuleAddFunction), METH_O,
|
|
"Register a function with this module."},
|
|
{nullptr},
|
|
};
|
|
|
|
// clang-format off
|
|
static PyTypeObject PyQueryModuleType = {
|
|
PyVarObject_HEAD_INIT(nullptr, 0)
|
|
.tp_name = "_mgp.Module",
|
|
.tp_basicsize = sizeof(PyQueryModule),
|
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
|
.tp_doc = "Wraps struct mgp_module.",
|
|
.tp_methods = PyQueryModuleMethods,
|
|
};
|
|
// clang-format on
|
|
|
|
PyObject *MakePyQueryModule(mgp_module *module) {
|
|
MG_ASSERT(module);
|
|
auto *py_query_module = PyObject_New(PyQueryModule, &PyQueryModuleType);
|
|
if (!py_query_module) return nullptr;
|
|
py_query_module->module = module;
|
|
return reinterpret_cast<PyObject *>(py_query_module);
|
|
}
|
|
|
|
PyObject *PyMgpModuleTypeNullable(PyObject *mod, PyObject *obj) {
|
|
if (Py_TYPE(obj) != &PyCypherTypeType) {
|
|
PyErr_SetString(PyExc_TypeError, "Expected a _mgp.Type.");
|
|
return nullptr;
|
|
}
|
|
auto *py_type = reinterpret_cast<PyCypherType *>(obj);
|
|
mgp_type *type{nullptr};
|
|
if (RaiseExceptionFromErrorCode(mgp_type_nullable(py_type->type, &type))) {
|
|
return nullptr;
|
|
}
|
|
return MakePyCypherType(type);
|
|
}
|
|
|
|
PyObject *PyMgpModuleTypeList(PyObject *mod, PyObject *obj) {
|
|
if (Py_TYPE(obj) != &PyCypherTypeType) {
|
|
PyErr_SetString(PyExc_TypeError, "Expected a _mgp.Type.");
|
|
return nullptr;
|
|
}
|
|
auto *py_type = reinterpret_cast<PyCypherType *>(obj);
|
|
mgp_type *type{nullptr};
|
|
if (RaiseExceptionFromErrorCode(mgp_type_list(py_type->type, &type))) {
|
|
return nullptr;
|
|
}
|
|
return MakePyCypherType(type);
|
|
}
|
|
|
|
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
|
#define DEFINE_PY_MGP_MODULE_TYPE(capital_type, small_type) \
|
|
PyObject *PyMgpModuleType##capital_type(PyObject * /*mod*/, PyObject *Py_UNUSED(ignored)) { \
|
|
mgp_type *type{nullptr}; \
|
|
if (RaiseExceptionFromErrorCode(mgp_type_##small_type(&type))) { \
|
|
return nullptr; \
|
|
} \
|
|
return MakePyCypherType(type); \
|
|
}
|
|
|
|
DEFINE_PY_MGP_MODULE_TYPE(Any, any);
|
|
DEFINE_PY_MGP_MODULE_TYPE(Bool, bool);
|
|
DEFINE_PY_MGP_MODULE_TYPE(String, string);
|
|
DEFINE_PY_MGP_MODULE_TYPE(Int, int);
|
|
DEFINE_PY_MGP_MODULE_TYPE(Float, float);
|
|
DEFINE_PY_MGP_MODULE_TYPE(Number, number);
|
|
DEFINE_PY_MGP_MODULE_TYPE(Map, map);
|
|
DEFINE_PY_MGP_MODULE_TYPE(Node, node);
|
|
DEFINE_PY_MGP_MODULE_TYPE(Relationship, relationship);
|
|
DEFINE_PY_MGP_MODULE_TYPE(Path, path);
|
|
DEFINE_PY_MGP_MODULE_TYPE(Date, date);
|
|
DEFINE_PY_MGP_MODULE_TYPE(LocalTime, local_time);
|
|
DEFINE_PY_MGP_MODULE_TYPE(LocalDateTime, local_date_time);
|
|
DEFINE_PY_MGP_MODULE_TYPE(Duration, duration);
|
|
|
|
static PyMethodDef PyMgpModuleMethods[] = {
|
|
{"type_nullable", PyMgpModuleTypeNullable, METH_O,
|
|
"Build a type representing either a `null` value or a value of given "
|
|
"type."},
|
|
{"type_list", PyMgpModuleTypeList, METH_O, "Build a type representing a list of values of given type."},
|
|
{"type_any", PyMgpModuleTypeAny, METH_NOARGS, "Get the type representing any value that isn't `null`."},
|
|
{"type_bool", PyMgpModuleTypeBool, METH_NOARGS, "Get the type representing boolean values."},
|
|
{"type_string", PyMgpModuleTypeString, METH_NOARGS, "Get the type representing string values."},
|
|
{"type_int", PyMgpModuleTypeInt, METH_NOARGS, "Get the type representing integer values."},
|
|
{"type_float", PyMgpModuleTypeFloat, METH_NOARGS, "Get the type representing floating-point values."},
|
|
{"type_number", PyMgpModuleTypeNumber, METH_NOARGS, "Get the type representing any number value."},
|
|
{"type_map", PyMgpModuleTypeMap, METH_NOARGS, "Get the type representing map values."},
|
|
{"type_node", PyMgpModuleTypeNode, METH_NOARGS, "Get the type representing graph node values."},
|
|
{"type_relationship", PyMgpModuleTypeRelationship, METH_NOARGS,
|
|
"Get the type representing graph relationship values."},
|
|
{"type_path", PyMgpModuleTypePath, METH_NOARGS,
|
|
"Get the type representing a graph path (walk) from one node to another."},
|
|
{"type_date", PyMgpModuleTypeDate, METH_NOARGS, "Get the type representing a Date."},
|
|
{"type_local_time", PyMgpModuleTypeLocalTime, METH_NOARGS, "Get the type representing a LocalTime."},
|
|
{"type_local_date_time", PyMgpModuleTypeLocalDateTime, METH_NOARGS, "Get the type representing a LocalDateTime."},
|
|
{"type_duration", PyMgpModuleTypeDuration, METH_NOARGS, "Get the type representing a Duration."},
|
|
{nullptr},
|
|
};
|
|
|
|
// clang-format off
|
|
static PyModuleDef PyMgpModule = {
|
|
PyModuleDef_HEAD_INIT,
|
|
.m_name = "_mgp",
|
|
.m_doc = "Contains raw bindings to mg_procedure.h C API.",
|
|
.m_size = -1,
|
|
.m_methods = PyMgpModuleMethods,
|
|
};
|
|
// clang-format on
|
|
|
|
// clang-format off
|
|
struct PyPropertiesIterator {
|
|
PyObject_HEAD
|
|
mgp_properties_iterator *it;
|
|
PyGraph *py_graph;
|
|
};
|
|
// clang-format on
|
|
|
|
void PyPropertiesIteratorDealloc(PyPropertiesIterator *self) {
|
|
MG_ASSERT(self->it);
|
|
MG_ASSERT(self->py_graph);
|
|
// Avoid invoking `mgp_properties_iterator_destroy` if we are not in valid
|
|
// execution context. The query execution should free all memory used during
|
|
// execution, so we may cause a double free issue.
|
|
if (self->py_graph->graph) mgp_properties_iterator_destroy(self->it);
|
|
Py_DECREF(self->py_graph);
|
|
Py_TYPE(self)->tp_free(self);
|
|
}
|
|
|
|
PyObject *PyPropertiesIteratorGet(PyPropertiesIterator *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(self->it);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
mgp_property *property{nullptr};
|
|
if (RaiseExceptionFromErrorCode(mgp_properties_iterator_get(self->it, &property))) {
|
|
return nullptr;
|
|
}
|
|
if (property == nullptr) {
|
|
Py_INCREF(Py_None);
|
|
return Py_None;
|
|
}
|
|
py::Object py_name(PyUnicode_FromString(property->name));
|
|
if (!py_name) return nullptr;
|
|
auto py_value = MgpValueToPyObject(*property->value, self->py_graph);
|
|
if (!py_value) return nullptr;
|
|
return PyTuple_Pack(2, py_name.Ptr(), py_value.Ptr());
|
|
}
|
|
|
|
PyObject *PyPropertiesIteratorNext(PyPropertiesIterator *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(self->it);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
mgp_property *property{nullptr};
|
|
if (RaiseExceptionFromErrorCode(mgp_properties_iterator_next(self->it, &property))) {
|
|
return nullptr;
|
|
}
|
|
if (property == nullptr) {
|
|
Py_INCREF(Py_None);
|
|
return Py_None;
|
|
}
|
|
py::Object py_name(PyUnicode_FromString(property->name));
|
|
if (!py_name) return nullptr;
|
|
auto py_value = MgpValueToPyObject(*property->value, self->py_graph);
|
|
if (!py_value) return nullptr;
|
|
return PyTuple_Pack(2, py_name.Ptr(), py_value.Ptr());
|
|
}
|
|
|
|
static PyMethodDef PyPropertiesIteratorMethods[] = {
|
|
{"get", reinterpret_cast<PyCFunction>(PyPropertiesIteratorGet), METH_NOARGS,
|
|
"Get the current proprety pointed to by the iterator or return None."},
|
|
{"next", reinterpret_cast<PyCFunction>(PyPropertiesIteratorNext), METH_NOARGS,
|
|
"Advance the iterator to the next property and return it."},
|
|
{nullptr},
|
|
};
|
|
|
|
// clang-format off
|
|
static PyTypeObject PyPropertiesIteratorType = {
|
|
PyVarObject_HEAD_INIT(nullptr, 0)
|
|
.tp_name = "_mgp.PropertiesIterator",
|
|
.tp_basicsize = sizeof(PyPropertiesIterator),
|
|
.tp_dealloc = reinterpret_cast<destructor>(PyPropertiesIteratorDealloc),
|
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
|
.tp_doc = "Wraps struct mgp_properties_iterator.",
|
|
.tp_methods = PyPropertiesIteratorMethods,
|
|
};
|
|
// clang-format on
|
|
|
|
// clang-format off
|
|
struct PyEdge {
|
|
PyObject_HEAD
|
|
mgp_edge *edge;
|
|
PyGraph *py_graph;
|
|
};
|
|
// clang-format on
|
|
|
|
PyObject *PyEdgeGetTypeName(PyEdge *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(self);
|
|
MG_ASSERT(self->edge);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
mgp_edge_type edge_type{nullptr};
|
|
if (RaiseExceptionFromErrorCode(mgp_edge_get_type(self->edge, &edge_type))) {
|
|
return nullptr;
|
|
}
|
|
return PyUnicode_FromString(edge_type.name);
|
|
}
|
|
|
|
PyObject *PyEdgeFromVertex(PyEdge *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(self);
|
|
MG_ASSERT(self->edge);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
return MakePyVertex(self->edge->from, self->py_graph);
|
|
}
|
|
|
|
PyObject *PyEdgeToVertex(PyEdge *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(self);
|
|
MG_ASSERT(self->edge);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
return MakePyVertex(self->edge->to, self->py_graph);
|
|
}
|
|
|
|
void PyEdgeDealloc(PyEdge *self) {
|
|
MG_ASSERT(self->edge);
|
|
MG_ASSERT(self->py_graph);
|
|
// Avoid invoking `mgp_edge_destroy` if we are not in valid execution context.
|
|
// The query execution should free all memory used during execution, so we may
|
|
// cause a double free issue.
|
|
if (self->py_graph->graph) mgp_edge_destroy(self->edge);
|
|
Py_DECREF(self->py_graph);
|
|
Py_TYPE(self)->tp_free(self);
|
|
}
|
|
|
|
PyObject *PyEdgeIsValid(PyEdge *self, PyObject *Py_UNUSED(ignored)) {
|
|
return PyBool_FromLong(self->py_graph && self->py_graph->graph);
|
|
}
|
|
|
|
PyObject *PyEdgeUnderlyingGraphIsMutable(PyEdge *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(self);
|
|
MG_ASSERT(self->edge);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
return PyBool_FromLong(CallBool(mgp_graph_is_mutable, self->py_graph->graph));
|
|
}
|
|
|
|
PyObject *PyEdgeGetId(PyEdge *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(self);
|
|
MG_ASSERT(self->edge);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
mgp_edge_id edge_id{0};
|
|
if (RaiseExceptionFromErrorCode(mgp_edge_get_id(self->edge, &edge_id))) {
|
|
return nullptr;
|
|
}
|
|
return PyLong_FromLongLong(edge_id.as_int);
|
|
}
|
|
|
|
PyObject *PyEdgeIterProperties(PyEdge *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(self);
|
|
MG_ASSERT(self->edge);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
mgp_properties_iterator *properties_it{nullptr};
|
|
if (RaiseExceptionFromErrorCode(mgp_edge_iter_properties(self->edge, self->py_graph->memory, &properties_it))) {
|
|
return nullptr;
|
|
}
|
|
auto *py_properties_it = PyObject_New(PyPropertiesIterator, &PyPropertiesIteratorType);
|
|
if (!py_properties_it) {
|
|
mgp_properties_iterator_destroy(properties_it);
|
|
return nullptr;
|
|
}
|
|
py_properties_it->it = properties_it;
|
|
Py_INCREF(self->py_graph);
|
|
py_properties_it->py_graph = self->py_graph;
|
|
return reinterpret_cast<PyObject *>(py_properties_it);
|
|
}
|
|
|
|
PyObject *PyEdgeGetProperty(PyEdge *self, PyObject *args) {
|
|
MG_ASSERT(self);
|
|
MG_ASSERT(self->edge);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
const char *prop_name = nullptr;
|
|
if (!PyArg_ParseTuple(args, "s", &prop_name)) return nullptr;
|
|
mgp_value *prop_value{nullptr};
|
|
if (RaiseExceptionFromErrorCode(mgp_edge_get_property(self->edge, prop_name, self->py_graph->memory, &prop_value))) {
|
|
return nullptr;
|
|
}
|
|
auto py_prop_value = MgpValueToPyObject(*prop_value, self->py_graph);
|
|
mgp_value_destroy(prop_value);
|
|
return py_prop_value.Steal();
|
|
}
|
|
|
|
PyObject *PyEdgeSetProperty(PyEdge *self, PyObject *args) {
|
|
MG_ASSERT(self);
|
|
MG_ASSERT(self->edge);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
const char *prop_name = nullptr;
|
|
PyObject *py_value{nullptr};
|
|
if (!PyArg_ParseTuple(args, "sO", &prop_name, &py_value)) {
|
|
return nullptr;
|
|
}
|
|
MgpUniquePtr<mgp_value> prop_value{PyObjectToMgpValueWithPythonExceptions(py_value, self->py_graph->memory),
|
|
mgp_value_destroy};
|
|
|
|
if (prop_value == nullptr ||
|
|
RaiseExceptionFromErrorCode(mgp_edge_set_property(self->edge, prop_name, prop_value.get()))) {
|
|
return nullptr;
|
|
}
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
static PyMethodDef PyEdgeMethods[] = {
|
|
{"__reduce__", reinterpret_cast<PyCFunction>(DisallowPickleAndCopy), METH_NOARGS, "__reduce__ is not supported."},
|
|
{"is_valid", reinterpret_cast<PyCFunction>(PyEdgeIsValid), METH_NOARGS,
|
|
"Return True if the edge is in valid context and may be used."},
|
|
{"underlying_graph_is_mutable", reinterpret_cast<PyCFunction>(PyEdgeUnderlyingGraphIsMutable), METH_NOARGS,
|
|
"Return True if the edge is mutable and can be modified."},
|
|
{"get_id", reinterpret_cast<PyCFunction>(PyEdgeGetId), METH_NOARGS, "Return edge id."},
|
|
{"get_type_name", reinterpret_cast<PyCFunction>(PyEdgeGetTypeName), METH_NOARGS, "Return the edge's type name."},
|
|
{"from_vertex", reinterpret_cast<PyCFunction>(PyEdgeFromVertex), METH_NOARGS, "Return the edge's source vertex."},
|
|
{"to_vertex", reinterpret_cast<PyCFunction>(PyEdgeToVertex), METH_NOARGS, "Return the edge's destination vertex."},
|
|
{"iter_properties", reinterpret_cast<PyCFunction>(PyEdgeIterProperties), METH_NOARGS,
|
|
"Return _mgp.PropertiesIterator for this edge."},
|
|
{"get_property", reinterpret_cast<PyCFunction>(PyEdgeGetProperty), METH_VARARGS,
|
|
"Return edge property with given name."},
|
|
{"set_property", reinterpret_cast<PyCFunction>(PyEdgeSetProperty), METH_VARARGS,
|
|
"Set the value of the property on the edge."},
|
|
{nullptr},
|
|
};
|
|
|
|
PyObject *PyEdgeRichCompare(PyObject *self, PyObject *other, int op);
|
|
|
|
// clang-format off
|
|
static PyTypeObject PyEdgeType = {
|
|
PyVarObject_HEAD_INIT(nullptr, 0)
|
|
.tp_name = "_mgp.Edge",
|
|
.tp_basicsize = sizeof(PyEdge),
|
|
.tp_dealloc = reinterpret_cast<destructor>(PyEdgeDealloc),
|
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
|
.tp_doc = "Wraps struct mgp_edge.",
|
|
.tp_richcompare = PyEdgeRichCompare,
|
|
.tp_methods = PyEdgeMethods,
|
|
};
|
|
// clang-format on
|
|
|
|
PyObject *MakePyEdgeWithoutCopy(mgp_edge &edge, PyGraph *py_graph) {
|
|
MG_ASSERT(py_graph);
|
|
MG_ASSERT(py_graph->graph && py_graph->memory);
|
|
MG_ASSERT(edge.GetMemoryResource() == py_graph->memory->impl);
|
|
auto *py_edge = PyObject_New(PyEdge, &PyEdgeType); // NOLINT(cppcoreguidelines-pro-type-cstyle-cast)
|
|
if (!py_edge) return nullptr;
|
|
py_edge->edge = &edge;
|
|
py_edge->py_graph = py_graph;
|
|
Py_INCREF(py_graph); // NOLINT(cppcoreguidelines-pro-type-cstyle-cast)
|
|
return reinterpret_cast<PyObject *>(py_edge);
|
|
}
|
|
|
|
/// Create an instance of `_mgp.Edge` class.
|
|
///
|
|
/// The created instance references an existing `_mgp.Graph` instance, which
|
|
/// marks the execution context.
|
|
PyObject *MakePyEdge(mgp_edge &edge, PyGraph *py_graph) {
|
|
MG_ASSERT(py_graph);
|
|
MG_ASSERT(py_graph->graph && py_graph->memory);
|
|
MgpUniquePtr<mgp_edge> edge_copy{nullptr, mgp_edge_destroy};
|
|
if (RaiseExceptionFromErrorCode(CreateMgpObject(edge_copy, mgp_edge_copy, &edge, py_graph->memory))) {
|
|
return nullptr;
|
|
}
|
|
auto *py_edge = MakePyEdgeWithoutCopy(*edge_copy, py_graph);
|
|
if (py_edge != nullptr) {
|
|
static_cast<void>(edge_copy.release());
|
|
}
|
|
return py_edge;
|
|
}
|
|
|
|
PyObject *PyEdgeRichCompare(PyObject *self, PyObject *other, int op) {
|
|
MG_ASSERT(self);
|
|
MG_ASSERT(other);
|
|
|
|
if (Py_TYPE(self) != &PyEdgeType || Py_TYPE(other) != &PyEdgeType || op != Py_EQ) {
|
|
Py_RETURN_NOTIMPLEMENTED;
|
|
}
|
|
|
|
auto *e1 = reinterpret_cast<PyEdge *>(self);
|
|
auto *e2 = reinterpret_cast<PyEdge *>(other);
|
|
MG_ASSERT(e1->edge);
|
|
MG_ASSERT(e2->edge);
|
|
return PyBool_FromLong(Call<int>(mgp_edge_equal, e1->edge, e2->edge));
|
|
}
|
|
|
|
// clang-format off
|
|
struct PyVertex {
|
|
PyObject_HEAD
|
|
mgp_vertex *vertex;
|
|
PyGraph *py_graph;
|
|
};
|
|
// clang-format on
|
|
|
|
void PyVertexDealloc(PyVertex *self) {
|
|
MG_ASSERT(self->vertex);
|
|
MG_ASSERT(self->py_graph);
|
|
// Avoid invoking `mgp_vertex_destroy` if we are not in valid execution
|
|
// context. The query execution should free all memory used during
|
|
// execution, so we may cause a double free issue.
|
|
if (self->py_graph->graph) mgp_vertex_destroy(self->vertex);
|
|
Py_DECREF(self->py_graph);
|
|
Py_TYPE(self)->tp_free(self);
|
|
}
|
|
|
|
PyObject *PyVertexIsValid(PyVertex *self, PyObject *Py_UNUSED(ignored)) {
|
|
return PyBool_FromLong(self->py_graph && self->py_graph->graph);
|
|
}
|
|
|
|
PyObject *PyVertexUnderlyingGraphIsMutable(PyVertex *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(self);
|
|
MG_ASSERT(self->vertex);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
return PyBool_FromLong(CallBool(mgp_graph_is_mutable, self->py_graph->graph));
|
|
}
|
|
|
|
PyObject *PyVertexGetId(PyVertex *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(self);
|
|
MG_ASSERT(self->vertex);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
mgp_vertex_id id{};
|
|
if (RaiseExceptionFromErrorCode(mgp_vertex_get_id(self->vertex, &id))) {
|
|
return nullptr;
|
|
}
|
|
return PyLong_FromLongLong(id.as_int);
|
|
}
|
|
|
|
PyObject *PyVertexLabelsCount(PyVertex *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(self);
|
|
MG_ASSERT(self->vertex);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
size_t label_count{0};
|
|
if (RaiseExceptionFromErrorCode(mgp_vertex_labels_count(self->vertex, &label_count))) {
|
|
return nullptr;
|
|
}
|
|
return PyLong_FromSize_t(label_count);
|
|
}
|
|
|
|
PyObject *PyVertexLabelAt(PyVertex *self, PyObject *args) {
|
|
MG_ASSERT(self);
|
|
MG_ASSERT(self->vertex);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
static_assert(std::numeric_limits<Py_ssize_t>::max() <= std::numeric_limits<size_t>::max());
|
|
Py_ssize_t id;
|
|
if (!PyArg_ParseTuple(args, "n", &id)) {
|
|
return nullptr;
|
|
}
|
|
mgp_label label{nullptr};
|
|
if (RaiseExceptionFromErrorCode(mgp_vertex_label_at(self->vertex, id, &label))) {
|
|
return nullptr;
|
|
}
|
|
return PyUnicode_FromString(label.name);
|
|
}
|
|
|
|
PyObject *PyVertexIterInEdges(PyVertex *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(self);
|
|
MG_ASSERT(self->vertex);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
mgp_edges_iterator *edges_it{nullptr};
|
|
if (RaiseExceptionFromErrorCode(mgp_vertex_iter_in_edges(self->vertex, self->py_graph->memory, &edges_it))) {
|
|
return nullptr;
|
|
}
|
|
auto *py_edges_it = PyObject_New(PyEdgesIterator, &PyEdgesIteratorType);
|
|
if (!py_edges_it) {
|
|
mgp_edges_iterator_destroy(edges_it);
|
|
return nullptr;
|
|
}
|
|
py_edges_it->it = edges_it;
|
|
Py_INCREF(self->py_graph);
|
|
py_edges_it->py_graph = self->py_graph;
|
|
return reinterpret_cast<PyObject *>(py_edges_it);
|
|
}
|
|
|
|
PyObject *PyVertexIterOutEdges(PyVertex *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(self);
|
|
MG_ASSERT(self->vertex);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
mgp_edges_iterator *edges_it{nullptr};
|
|
if (RaiseExceptionFromErrorCode(mgp_vertex_iter_out_edges(self->vertex, self->py_graph->memory, &edges_it))) {
|
|
return nullptr;
|
|
}
|
|
auto *py_edges_it = PyObject_New(PyEdgesIterator, &PyEdgesIteratorType);
|
|
if (!py_edges_it) {
|
|
mgp_edges_iterator_destroy(edges_it);
|
|
return nullptr;
|
|
}
|
|
py_edges_it->it = edges_it;
|
|
Py_INCREF(self->py_graph);
|
|
py_edges_it->py_graph = self->py_graph;
|
|
return reinterpret_cast<PyObject *>(py_edges_it);
|
|
}
|
|
|
|
PyObject *PyVertexIterProperties(PyVertex *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(self);
|
|
MG_ASSERT(self->vertex);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
mgp_properties_iterator *properties_it{nullptr};
|
|
if (RaiseExceptionFromErrorCode(mgp_vertex_iter_properties(self->vertex, self->py_graph->memory, &properties_it))) {
|
|
return nullptr;
|
|
}
|
|
auto *py_properties_it = PyObject_New(PyPropertiesIterator, &PyPropertiesIteratorType);
|
|
if (!py_properties_it) {
|
|
mgp_properties_iterator_destroy(properties_it);
|
|
return nullptr;
|
|
}
|
|
py_properties_it->it = properties_it;
|
|
Py_INCREF(self->py_graph);
|
|
py_properties_it->py_graph = self->py_graph;
|
|
return reinterpret_cast<PyObject *>(py_properties_it);
|
|
}
|
|
|
|
PyObject *PyVertexGetProperty(PyVertex *self, PyObject *args) {
|
|
MG_ASSERT(self);
|
|
MG_ASSERT(self->vertex);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
const char *prop_name{nullptr};
|
|
if (!PyArg_ParseTuple(args, "s", &prop_name)) {
|
|
return nullptr;
|
|
}
|
|
mgp_value *prop_value{nullptr};
|
|
if (RaiseExceptionFromErrorCode(
|
|
mgp_vertex_get_property(self->vertex, prop_name, self->py_graph->memory, &prop_value))) {
|
|
return nullptr;
|
|
}
|
|
auto py_prop_value = MgpValueToPyObject(*prop_value, self->py_graph);
|
|
mgp_value_destroy(prop_value);
|
|
return py_prop_value.Steal();
|
|
}
|
|
|
|
PyObject *PyVertexSetProperty(PyVertex *self, PyObject *args) {
|
|
MG_ASSERT(self);
|
|
MG_ASSERT(self->vertex);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
const char *prop_name = nullptr;
|
|
PyObject *py_value{nullptr};
|
|
if (!PyArg_ParseTuple(args, "sO", &prop_name, &py_value)) {
|
|
return nullptr;
|
|
}
|
|
MgpUniquePtr<mgp_value> prop_value{PyObjectToMgpValueWithPythonExceptions(py_value, self->py_graph->memory),
|
|
mgp_value_destroy};
|
|
|
|
if (prop_value == nullptr ||
|
|
RaiseExceptionFromErrorCode(mgp_vertex_set_property(self->vertex, prop_name, prop_value.get()))) {
|
|
return nullptr;
|
|
}
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
PyObject *PyVertexAddLabel(PyVertex *self, PyObject *args) {
|
|
MG_ASSERT(self);
|
|
MG_ASSERT(self->vertex);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
const char *label_name = nullptr;
|
|
if (!PyArg_ParseTuple(args, "s", &label_name)) {
|
|
return nullptr;
|
|
}
|
|
if (RaiseExceptionFromErrorCode(mgp_vertex_add_label(self->vertex, mgp_label{label_name}))) {
|
|
return nullptr;
|
|
}
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
PyObject *PyVertexRemoveLabel(PyVertex *self, PyObject *args) {
|
|
MG_ASSERT(self);
|
|
MG_ASSERT(self->vertex);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
const char *label_name = nullptr;
|
|
if (!PyArg_ParseTuple(args, "s", &label_name)) {
|
|
return nullptr;
|
|
}
|
|
if (RaiseExceptionFromErrorCode(mgp_vertex_remove_label(self->vertex, mgp_label{label_name}))) {
|
|
return nullptr;
|
|
}
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
static PyMethodDef PyVertexMethods[] = {
|
|
{"__reduce__", reinterpret_cast<PyCFunction>(DisallowPickleAndCopy), METH_NOARGS, "__reduce__ is not supported."},
|
|
{"is_valid", reinterpret_cast<PyCFunction>(PyVertexIsValid), METH_NOARGS,
|
|
"Return True if the vertex is in valid context and may be used."},
|
|
{"underlying_graph_is_mutable", reinterpret_cast<PyCFunction>(PyVertexUnderlyingGraphIsMutable), METH_NOARGS,
|
|
"Return True if the vertex is mutable and can be modified."},
|
|
{"get_id", reinterpret_cast<PyCFunction>(PyVertexGetId), METH_NOARGS, "Return vertex id."},
|
|
{"labels_count", reinterpret_cast<PyCFunction>(PyVertexLabelsCount), METH_NOARGS,
|
|
"Return number of lables of a vertex."},
|
|
{"label_at", reinterpret_cast<PyCFunction>(PyVertexLabelAt), METH_VARARGS,
|
|
"Return label of a vertex on a given index."},
|
|
{"add_label", reinterpret_cast<PyCFunction>(PyVertexAddLabel), METH_VARARGS, "Add the label to the vertex."},
|
|
{"remove_label", reinterpret_cast<PyCFunction>(PyVertexRemoveLabel), METH_VARARGS,
|
|
"Remove the label from the vertex."},
|
|
{"iter_in_edges", reinterpret_cast<PyCFunction>(PyVertexIterInEdges), METH_NOARGS,
|
|
"Return _mgp.EdgesIterator for in edges."},
|
|
{"iter_out_edges", reinterpret_cast<PyCFunction>(PyVertexIterOutEdges), METH_NOARGS,
|
|
"Return _mgp.EdgesIterator for out edges."},
|
|
{"iter_properties", reinterpret_cast<PyCFunction>(PyVertexIterProperties), METH_NOARGS,
|
|
"Return _mgp.PropertiesIterator for this vertex."},
|
|
{"get_property", reinterpret_cast<PyCFunction>(PyVertexGetProperty), METH_VARARGS,
|
|
"Return vertex property with given name."},
|
|
{"set_property", reinterpret_cast<PyCFunction>(PyVertexSetProperty), METH_VARARGS,
|
|
"Set the value of the property on the vertex."},
|
|
{nullptr},
|
|
};
|
|
|
|
PyObject *PyVertexRichCompare(PyObject *self, PyObject *other, int op);
|
|
|
|
// clang-format off
|
|
static PyTypeObject PyVertexType = {
|
|
PyVarObject_HEAD_INIT(nullptr, 0)
|
|
.tp_name = "_mgp.Vertex",
|
|
.tp_basicsize = sizeof(PyVertex),
|
|
.tp_dealloc = reinterpret_cast<destructor>(PyVertexDealloc),
|
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
|
.tp_doc = "Wraps struct mgp_vertex.",
|
|
.tp_richcompare = PyVertexRichCompare,
|
|
.tp_methods = PyVertexMethods,
|
|
};
|
|
// clang-format on
|
|
|
|
PyObject *MakePyVertexWithoutCopy(mgp_vertex &vertex, PyGraph *py_graph) {
|
|
MG_ASSERT(py_graph);
|
|
MG_ASSERT(py_graph->graph && py_graph->memory);
|
|
MG_ASSERT(vertex.GetMemoryResource() == py_graph->memory->impl);
|
|
auto *py_vertex = PyObject_New(PyVertex, &PyVertexType);
|
|
if (!py_vertex) return nullptr;
|
|
py_vertex->vertex = &vertex;
|
|
py_vertex->py_graph = py_graph;
|
|
Py_INCREF(py_graph);
|
|
return reinterpret_cast<PyObject *>(py_vertex);
|
|
}
|
|
|
|
PyObject *MakePyVertex(mgp_vertex &vertex, PyGraph *py_graph) {
|
|
MG_ASSERT(py_graph);
|
|
MG_ASSERT(py_graph->graph && py_graph->memory);
|
|
|
|
MgpUniquePtr<mgp_vertex> vertex_copy{nullptr, mgp_vertex_destroy};
|
|
if (RaiseExceptionFromErrorCode(CreateMgpObject(vertex_copy, mgp_vertex_copy, &vertex, py_graph->memory))) {
|
|
return nullptr;
|
|
}
|
|
auto *py_vertex = MakePyVertexWithoutCopy(*vertex_copy, py_graph);
|
|
if (py_vertex != nullptr) {
|
|
static_cast<void>(vertex_copy.release());
|
|
}
|
|
return py_vertex;
|
|
}
|
|
|
|
PyObject *PyVertexRichCompare(PyObject *self, PyObject *other, int op) {
|
|
MG_ASSERT(self);
|
|
MG_ASSERT(other);
|
|
|
|
if (Py_TYPE(self) != &PyVertexType || Py_TYPE(other) != &PyVertexType || op != Py_EQ) {
|
|
Py_RETURN_NOTIMPLEMENTED;
|
|
}
|
|
|
|
auto *v1 = reinterpret_cast<PyVertex *>(self);
|
|
auto *v2 = reinterpret_cast<PyVertex *>(other);
|
|
MG_ASSERT(v1->vertex);
|
|
MG_ASSERT(v2->vertex);
|
|
|
|
return PyBool_FromLong(Call<int>(mgp_vertex_equal, v1->vertex, v2->vertex));
|
|
}
|
|
|
|
// clang-format off
|
|
struct PyPath {
|
|
PyObject_HEAD
|
|
mgp_path *path;
|
|
PyGraph *py_graph;
|
|
};
|
|
// clang-format on
|
|
|
|
void PyPathDealloc(PyPath *self) {
|
|
MG_ASSERT(self->path);
|
|
MG_ASSERT(self->py_graph);
|
|
// Avoid invoking `mgp_path_destroy` if we are not in valid execution
|
|
// context. The query execution should free all memory used during
|
|
// execution, so we may cause a double free issue.
|
|
if (self->py_graph->graph) mgp_path_destroy(self->path);
|
|
Py_DECREF(self->py_graph);
|
|
Py_TYPE(self)->tp_free(self);
|
|
}
|
|
|
|
PyObject *PyPathIsValid(PyPath *self, PyObject *Py_UNUSED(ignored)) {
|
|
return PyBool_FromLong(self->py_graph && self->py_graph->graph);
|
|
}
|
|
|
|
PyObject *PyPathMakeWithStart(PyTypeObject *type, PyObject *vertex);
|
|
|
|
PyObject *PyPathExpand(PyPath *self, PyObject *edge) {
|
|
MG_ASSERT(self->path);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
if (Py_TYPE(edge) != &PyEdgeType) {
|
|
PyErr_SetString(PyExc_TypeError, "Expected a _mgp.Edge.");
|
|
return nullptr;
|
|
}
|
|
auto *py_edge = reinterpret_cast<PyEdge *>(edge);
|
|
|
|
if (RaiseExceptionFromErrorCode(mgp_path_expand(self->path, py_edge->edge))) {
|
|
return nullptr;
|
|
}
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
PyObject *PyPathSize(PyPath *self, PyObject *Py_UNUSED(ignored)) {
|
|
MG_ASSERT(self->path);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
return PyLong_FromSize_t(Call<size_t>(mgp_path_size, self->path));
|
|
}
|
|
|
|
PyObject *PyPathVertexAt(PyPath *self, PyObject *args) {
|
|
MG_ASSERT(self->path);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
static_assert(std::numeric_limits<Py_ssize_t>::max() <= std::numeric_limits<size_t>::max());
|
|
Py_ssize_t i;
|
|
if (!PyArg_ParseTuple(args, "n", &i)) {
|
|
return nullptr;
|
|
}
|
|
mgp_vertex *vertex{nullptr};
|
|
if (RaiseExceptionFromErrorCode(mgp_path_vertex_at(self->path, i, &vertex))) {
|
|
return nullptr;
|
|
}
|
|
return MakePyVertex(*vertex, self->py_graph);
|
|
}
|
|
|
|
PyObject *PyPathEdgeAt(PyPath *self, PyObject *args) {
|
|
MG_ASSERT(self->path);
|
|
MG_ASSERT(self->py_graph);
|
|
MG_ASSERT(self->py_graph->graph);
|
|
static_assert(std::numeric_limits<Py_ssize_t>::max() <= std::numeric_limits<size_t>::max());
|
|
Py_ssize_t i;
|
|
if (!PyArg_ParseTuple(args, "n", &i)) {
|
|
return nullptr;
|
|
}
|
|
mgp_edge *edge{nullptr};
|
|
if (RaiseExceptionFromErrorCode(mgp_path_edge_at(self->path, i, &edge))) {
|
|
return nullptr;
|
|
}
|
|
return MakePyEdge(*edge, self->py_graph);
|
|
}
|
|
|
|
static PyMethodDef PyPathMethods[] = {
|
|
{"__reduce__", reinterpret_cast<PyCFunction>(DisallowPickleAndCopy), METH_NOARGS, "__reduce__ is not supported"},
|
|
{"is_valid", reinterpret_cast<PyCFunction>(PyPathIsValid), METH_NOARGS,
|
|
"Return True if Path is in valid context and may be used."},
|
|
{"make_with_start", reinterpret_cast<PyCFunction>(PyPathMakeWithStart), METH_O | METH_CLASS,
|
|
"Create a path with a starting vertex."},
|
|
{"expand", reinterpret_cast<PyCFunction>(PyPathExpand), METH_O,
|
|
"Append an edge continuing from the last vertex on the path."},
|
|
{"size", reinterpret_cast<PyCFunction>(PyPathSize), METH_NOARGS, "Return the number of edges in a mgp_path."},
|
|
{"vertex_at", reinterpret_cast<PyCFunction>(PyPathVertexAt), METH_VARARGS,
|
|
"Return the vertex from a path at given index."},
|
|
{"edge_at", reinterpret_cast<PyCFunction>(PyPathEdgeAt), METH_VARARGS,
|
|
"Return the edge from a path at given index."},
|
|
{nullptr},
|
|
};
|
|
|
|
// clang-format off
|
|
static PyTypeObject PyPathType = {
|
|
PyVarObject_HEAD_INIT(nullptr, 0)
|
|
.tp_name = "_mgp.Path",
|
|
.tp_basicsize = sizeof(PyPath),
|
|
.tp_dealloc = reinterpret_cast<destructor>(PyPathDealloc),
|
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
|
.tp_doc = "Wraps struct mgp_path.",
|
|
.tp_methods = PyPathMethods,
|
|
};
|
|
// clang-format on
|
|
|
|
PyObject *MakePyPath(mgp_path *path, PyGraph *py_graph) {
|
|
MG_ASSERT(path);
|
|
MG_ASSERT(py_graph->graph && py_graph->memory);
|
|
MG_ASSERT(path->GetMemoryResource() == py_graph->memory->impl);
|
|
auto *py_path = PyObject_New(PyPath, &PyPathType);
|
|
if (!py_path) return nullptr;
|
|
py_path->path = path;
|
|
py_path->py_graph = py_graph;
|
|
Py_INCREF(py_graph);
|
|
return reinterpret_cast<PyObject *>(py_path);
|
|
}
|
|
|
|
PyObject *MakePyPath(mgp_path &path, PyGraph *py_graph) {
|
|
MG_ASSERT(py_graph);
|
|
MG_ASSERT(py_graph->graph && py_graph->memory);
|
|
mgp_path *path_copy{nullptr};
|
|
|
|
if (RaiseExceptionFromErrorCode(mgp_path_copy(&path, py_graph->memory, &path_copy))) {
|
|
return nullptr;
|
|
}
|
|
auto *py_path = MakePyPath(path_copy, py_graph);
|
|
if (!py_path) mgp_path_destroy(path_copy);
|
|
return py_path;
|
|
}
|
|
|
|
PyObject *PyPathMakeWithStart(PyTypeObject *type, PyObject *vertex) {
|
|
if (type != &PyPathType) {
|
|
PyErr_SetString(PyExc_TypeError, "Expected '<class _mgp.Path>' as the first argument.");
|
|
return nullptr;
|
|
}
|
|
if (Py_TYPE(vertex) != &PyVertexType) {
|
|
PyErr_SetString(PyExc_TypeError, "Expected a '_mgp.Vertex' as the second argument.");
|
|
return nullptr;
|
|
}
|
|
auto *py_vertex = reinterpret_cast<PyVertex *>(vertex);
|
|
mgp_path *path{nullptr};
|
|
if (RaiseExceptionFromErrorCode(mgp_path_make_with_start(py_vertex->vertex, py_vertex->py_graph->memory, &path))) {
|
|
return nullptr;
|
|
}
|
|
auto *py_path = MakePyPath(path, py_vertex->py_graph);
|
|
if (!py_path) mgp_path_destroy(path);
|
|
return py_path;
|
|
}
|
|
|
|
struct PyMgpError {
|
|
const char *name;
|
|
PyObject *&exception;
|
|
PyObject *&base;
|
|
const char *docstring;
|
|
};
|
|
|
|
bool AddModuleConstants(PyObject &module) {
|
|
// add source type constants
|
|
if (PyModule_AddIntConstant(&module, "SOURCE_TYPE_KAFKA", static_cast<int64_t>(mgp_source_type::KAFKA))) {
|
|
return false;
|
|
}
|
|
if (PyModule_AddIntConstant(&module, "SOURCE_TYPE_PULSAR", static_cast<int64_t>(mgp_source_type::PULSAR))) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
PyObject *PyInitMgpModule() {
|
|
PyObject *mgp = PyModule_Create(&PyMgpModule);
|
|
if (!mgp) return nullptr;
|
|
auto register_type = [mgp](auto *type, const auto *name) -> bool {
|
|
if (PyType_Ready(type) < 0) {
|
|
Py_DECREF(mgp);
|
|
return false;
|
|
}
|
|
Py_INCREF(type);
|
|
if (PyModule_AddObject(mgp, name, reinterpret_cast<PyObject *>(type)) < 0) {
|
|
Py_DECREF(type);
|
|
Py_DECREF(mgp);
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
if (!AddModuleConstants(*mgp)) return nullptr;
|
|
|
|
if (!register_type(&PyPropertiesIteratorType, "PropertiesIterator")) return nullptr;
|
|
if (!register_type(&PyVerticesIteratorType, "VerticesIterator")) return nullptr;
|
|
if (!register_type(&PyEdgesIteratorType, "EdgesIterator")) return nullptr;
|
|
if (!register_type(&PyGraphType, "Graph")) return nullptr;
|
|
if (!register_type(&PyEdgeType, "Edge")) return nullptr;
|
|
if (!register_type(&PyQueryProcType, "Proc")) return nullptr;
|
|
if (!register_type(&PyMagicFuncType, "Func")) return nullptr;
|
|
if (!register_type(&PyQueryModuleType, "Module")) return nullptr;
|
|
if (!register_type(&PyVertexType, "Vertex")) return nullptr;
|
|
if (!register_type(&PyPathType, "Path")) return nullptr;
|
|
if (!register_type(&PyCypherTypeType, "Type")) return nullptr;
|
|
if (!register_type(&PyMessagesType, "Messages")) return nullptr;
|
|
if (!register_type(&PyMessageType, "Message")) return nullptr;
|
|
|
|
std::array py_mgp_errors{
|
|
PyMgpError{"_mgp.UnknownError", gMgpUnknownError, PyExc_RuntimeError, nullptr},
|
|
PyMgpError{"_mgp.UnableToAllocateError", gMgpUnableToAllocateError, PyExc_MemoryError, nullptr},
|
|
PyMgpError{"_mgp.InsufficientBufferError", gMgpInsufficientBufferError, PyExc_BufferError, nullptr},
|
|
PyMgpError{"_mgp.OutOfRangeError", gMgpOutOfRangeError, PyExc_BufferError, nullptr},
|
|
PyMgpError{"_mgp.LogicErrorError", gMgpLogicErrorError, PyExc_RuntimeError, nullptr},
|
|
PyMgpError{"_mgp.DeletedObjectError", gMgpDeletedObjectError, PyExc_RuntimeError, nullptr},
|
|
PyMgpError{"_mgp.InvalidArgumentError", gMgpInvalidArgumentError, PyExc_ValueError, nullptr},
|
|
PyMgpError{"_mgp.KeyAlreadyExistsError", gMgpKeyAlreadyExistsError, PyExc_RuntimeError, nullptr},
|
|
PyMgpError{"_mgp.ImmutableObjectError", gMgpImmutableObjectError, PyExc_RuntimeError, nullptr},
|
|
PyMgpError{"_mgp.ValueConversionError", gMgpValueConversionError, PyExc_RuntimeError, nullptr},
|
|
PyMgpError{"_mgp.SerializationError", gMgpSerializationError, PyExc_RuntimeError, nullptr},
|
|
};
|
|
Py_INCREF(Py_None);
|
|
|
|
utils::OnScopeExit clean_up{[mgp, &py_mgp_errors] {
|
|
for (const auto &py_mgp_error : py_mgp_errors) {
|
|
Py_XDECREF(py_mgp_error.exception);
|
|
}
|
|
Py_DECREF(Py_None);
|
|
Py_DECREF(mgp);
|
|
}};
|
|
|
|
if (PyModule_AddObject(mgp, "_MODULE", Py_None) < 0) {
|
|
return nullptr;
|
|
}
|
|
|
|
auto register_custom_error = [mgp](PyMgpError &py_mgp_error) {
|
|
py_mgp_error.exception = PyErr_NewException(py_mgp_error.name, py_mgp_error.base, nullptr);
|
|
if (py_mgp_error.exception == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
const auto *name_in_module = std::string_view(py_mgp_error.name).substr(5).data();
|
|
return PyModule_AddObject(mgp, name_in_module, py_mgp_error.exception) == 0;
|
|
};
|
|
|
|
for (auto &py_mgp_error : py_mgp_errors) {
|
|
if (!register_custom_error(py_mgp_error)) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
clean_up.Disable();
|
|
|
|
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast)
|
|
PyDateTime_IMPORT;
|
|
|
|
return mgp;
|
|
}
|
|
|
|
namespace {
|
|
|
|
template <class TFun>
|
|
auto WithMgpModule(mgp_module *module_def, const TFun &fun) {
|
|
py::Object py_mgp(PyImport_ImportModule("_mgp"));
|
|
MG_ASSERT(py_mgp, "Expected builtin '_mgp' to be available for import");
|
|
py::Object py_mgp_module(py_mgp.GetAttr("_MODULE"));
|
|
MG_ASSERT(py_mgp_module, "Expected '_mgp' to have attribute '_MODULE'");
|
|
// NOTE: This check is not thread safe, but this should only go through
|
|
// ModuleRegistry::LoadModuleLibrary which ought to serialize loading.
|
|
MG_ASSERT(py_mgp_module.Ptr() == Py_None,
|
|
"Expected '_mgp._MODULE' to be None as we are just starting to "
|
|
"import a new module. Is some other thread also importing Python "
|
|
"modules?");
|
|
auto *py_query_module = MakePyQueryModule(module_def);
|
|
MG_ASSERT(py_query_module);
|
|
MG_ASSERT(py_mgp.SetAttr("_MODULE", py_query_module));
|
|
auto ret = fun();
|
|
auto maybe_exc = py::FetchError();
|
|
MG_ASSERT(py_mgp.SetAttr("_MODULE", Py_None));
|
|
if (maybe_exc) {
|
|
py::RestoreError(*maybe_exc);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
py::Object ImportPyModule(const char *name, mgp_module *module_def) {
|
|
return WithMgpModule(module_def, [name]() { return py::Object(PyImport_ImportModule(name)); });
|
|
}
|
|
|
|
py::Object ReloadPyModule(PyObject *py_module, mgp_module *module_def) {
|
|
return WithMgpModule(module_def, [py_module]() { return py::Object(PyImport_ReloadModule(py_module)); });
|
|
}
|
|
|
|
py::Object MgpValueToPyObject(const mgp_value &value, PyObject *py_graph) {
|
|
if (Py_TYPE(py_graph) != &PyGraphType) {
|
|
PyErr_SetString(PyExc_TypeError, "Expected a _mgp.Graph.");
|
|
return nullptr;
|
|
}
|
|
return MgpValueToPyObject(value, reinterpret_cast<PyGraph *>(py_graph));
|
|
}
|
|
|
|
py::Object MgpValueToPyObject(const mgp_value &value, PyGraph *py_graph) {
|
|
switch (value.type) {
|
|
case MGP_VALUE_TYPE_NULL:
|
|
Py_INCREF(Py_None);
|
|
return py::Object(Py_None);
|
|
case MGP_VALUE_TYPE_BOOL:
|
|
return py::Object(PyBool_FromLong(value.bool_v));
|
|
case MGP_VALUE_TYPE_INT:
|
|
return py::Object(PyLong_FromLongLong(value.int_v));
|
|
case MGP_VALUE_TYPE_DOUBLE:
|
|
return py::Object(PyFloat_FromDouble(value.double_v));
|
|
case MGP_VALUE_TYPE_STRING:
|
|
return py::Object(PyUnicode_FromString(value.string_v.c_str()));
|
|
case MGP_VALUE_TYPE_LIST:
|
|
return MgpListToPyTuple(value.list_v, py_graph);
|
|
case MGP_VALUE_TYPE_MAP: {
|
|
auto *map = value.map_v;
|
|
py::Object py_dict(PyDict_New());
|
|
if (!py_dict) {
|
|
return nullptr;
|
|
}
|
|
for (const auto &[key, val] : map->items) {
|
|
auto py_val = MgpValueToPyObject(val, py_graph);
|
|
if (!py_val) {
|
|
return nullptr;
|
|
}
|
|
// Unlike PyList_SET_ITEM, PyDict_SetItem does not steal the value.
|
|
if (PyDict_SetItemString(py_dict.Ptr(), key.c_str(), py_val.Ptr()) != 0) return nullptr;
|
|
}
|
|
return py_dict;
|
|
}
|
|
case MGP_VALUE_TYPE_VERTEX: {
|
|
py::Object py_mgp(PyImport_ImportModule("mgp"));
|
|
if (!py_mgp) return nullptr;
|
|
auto *v = value.vertex_v;
|
|
py::Object py_vertex(reinterpret_cast<PyObject *>(MakePyVertex(*v, py_graph)));
|
|
return py_mgp.CallMethod("Vertex", py_vertex);
|
|
}
|
|
case MGP_VALUE_TYPE_EDGE: {
|
|
py::Object py_mgp(PyImport_ImportModule("mgp"));
|
|
if (!py_mgp) return nullptr;
|
|
auto *e = value.edge_v;
|
|
py::Object py_edge(reinterpret_cast<PyObject *>(MakePyEdge(*e, py_graph)));
|
|
return py_mgp.CallMethod("Edge", py_edge);
|
|
}
|
|
case MGP_VALUE_TYPE_PATH: {
|
|
py::Object py_mgp(PyImport_ImportModule("mgp"));
|
|
if (!py_mgp) return nullptr;
|
|
auto *p = value.path_v;
|
|
py::Object py_path(reinterpret_cast<PyObject *>(MakePyPath(*p, py_graph)));
|
|
return py_mgp.CallMethod("Path", py_path);
|
|
}
|
|
case MGP_VALUE_TYPE_DATE: {
|
|
const auto &date = value.date_v->date;
|
|
py::Object py_date(PyDate_FromDate(date.year, date.month, date.day));
|
|
return py_date;
|
|
}
|
|
case MGP_VALUE_TYPE_LOCAL_TIME: {
|
|
const auto &local_time = value.local_time_v->local_time;
|
|
py::Object py_local_time(PyTime_FromTime(local_time.hour, local_time.minute, local_time.second,
|
|
local_time.millisecond * 1000 + local_time.microsecond));
|
|
return py_local_time;
|
|
}
|
|
case MGP_VALUE_TYPE_LOCAL_DATE_TIME: {
|
|
const auto &local_time = value.local_date_time_v->local_date_time.local_time;
|
|
const auto &date = value.local_date_time_v->local_date_time.date;
|
|
py::Object py_local_date_time(PyDateTime_FromDateAndTime(date.year, date.month, date.day, local_time.hour,
|
|
local_time.minute, local_time.second,
|
|
local_time.millisecond * 1000 + local_time.microsecond));
|
|
return py_local_date_time;
|
|
}
|
|
case MGP_VALUE_TYPE_DURATION: {
|
|
const auto &duration = value.duration_v->duration;
|
|
py::Object py_duration(PyDelta_FromDSU(0, 0, duration.microseconds));
|
|
return py_duration;
|
|
}
|
|
}
|
|
}
|
|
|
|
mgp_value *PyObjectToMgpValue(PyObject *o, mgp_memory *memory) {
|
|
auto py_seq_to_list = [memory](PyObject *seq, Py_ssize_t len, const auto &py_seq_get_item) {
|
|
static_assert(std::numeric_limits<Py_ssize_t>::max() <= std::numeric_limits<size_t>::max());
|
|
MgpUniquePtr<mgp_list> list{nullptr, &mgp_list_destroy};
|
|
if (const auto err = CreateMgpObject(list, mgp_list_make_empty, len, memory);
|
|
err == mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE) {
|
|
throw std::bad_alloc{};
|
|
} else if (err != mgp_error::MGP_ERROR_NO_ERROR) {
|
|
throw std::runtime_error{"Unexpected error during making mgp_list"};
|
|
}
|
|
for (Py_ssize_t i = 0; i < len; ++i) {
|
|
PyObject *e = py_seq_get_item(seq, i);
|
|
mgp_value *v{nullptr};
|
|
v = PyObjectToMgpValue(e, memory);
|
|
const auto err = mgp_list_append(list.get(), v);
|
|
mgp_value_destroy(v);
|
|
if (err != mgp_error::MGP_ERROR_NO_ERROR) {
|
|
if (err == mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE) {
|
|
throw std::bad_alloc{};
|
|
}
|
|
throw std::runtime_error{"Unexpected error during appending to mgp_list"};
|
|
}
|
|
}
|
|
mgp_value *v{nullptr};
|
|
if (const auto err = mgp_value_make_list(list.get(), &v); err == mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE) {
|
|
throw std::bad_alloc{};
|
|
} else if (err != mgp_error::MGP_ERROR_NO_ERROR) {
|
|
throw std::runtime_error{"Unexpected error during making mgp_value"};
|
|
}
|
|
static_cast<void>(list.release());
|
|
return v;
|
|
};
|
|
|
|
auto is_mgp_instance = [](PyObject *obj, const char *mgp_type_name) {
|
|
py::Object py_mgp(PyImport_ImportModule("mgp"));
|
|
if (!py_mgp) {
|
|
PyErr_Clear();
|
|
// This way we skip conversions of types from user-facing 'mgp' module.
|
|
return false;
|
|
}
|
|
auto mgp_type = py_mgp.GetAttr(mgp_type_name);
|
|
if (!mgp_type) {
|
|
PyErr_Clear();
|
|
std::stringstream ss;
|
|
ss << "'mgp' module is missing '" << mgp_type_name << "' type";
|
|
throw std::invalid_argument(ss.str());
|
|
}
|
|
int res = PyObject_IsInstance(obj, mgp_type.Ptr());
|
|
if (res == -1) {
|
|
PyErr_Clear();
|
|
std::stringstream ss;
|
|
ss << "Error when checking object is instance of 'mgp." << mgp_type_name << "' type";
|
|
throw std::invalid_argument(ss.str());
|
|
}
|
|
return static_cast<bool>(res);
|
|
};
|
|
|
|
mgp_value *mgp_v{nullptr};
|
|
mgp_error last_error{mgp_error::MGP_ERROR_NO_ERROR};
|
|
|
|
if (o == Py_None) {
|
|
last_error = mgp_value_make_null(memory, &mgp_v);
|
|
} else if (PyBool_Check(o)) {
|
|
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast) Py_True is defined with C-style cast
|
|
last_error = mgp_value_make_bool(static_cast<int>(o == Py_True), memory, &mgp_v);
|
|
} else if (PyLong_Check(o)) {
|
|
int64_t value = PyLong_AsLong(o);
|
|
if (PyErr_Occurred()) {
|
|
PyErr_Clear();
|
|
throw std::overflow_error("Python integer is out of range");
|
|
}
|
|
last_error = mgp_value_make_int(value, memory, &mgp_v);
|
|
} else if (PyFloat_Check(o)) {
|
|
last_error = mgp_value_make_double(PyFloat_AsDouble(o), memory, &mgp_v);
|
|
} else if (PyUnicode_Check(o)) { // NOLINT(hicpp-signed-bitwise)
|
|
last_error = mgp_value_make_string(PyUnicode_AsUTF8(o), memory, &mgp_v);
|
|
} else if (PyList_Check(o)) {
|
|
mgp_v = py_seq_to_list(o, PyList_Size(o), [](auto *list, const auto i) { return PyList_GET_ITEM(list, i); });
|
|
} else if (PyTuple_Check(o)) {
|
|
mgp_v = py_seq_to_list(o, PyTuple_Size(o), [](auto *tuple, const auto i) { return PyTuple_GET_ITEM(tuple, i); });
|
|
} else if (PyDict_Check(o)) { // NOLINT(hicpp-signed-bitwise)
|
|
MgpUniquePtr<mgp_map> map{nullptr, mgp_map_destroy};
|
|
const auto map_err = CreateMgpObject(map, mgp_map_make_empty, memory);
|
|
|
|
if (map_err == mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE) {
|
|
throw std::bad_alloc{};
|
|
}
|
|
if (map_err != mgp_error::MGP_ERROR_NO_ERROR) {
|
|
throw std::runtime_error{"Unexpected error during creating mgp_map"};
|
|
}
|
|
|
|
PyObject *key{nullptr};
|
|
PyObject *value{nullptr};
|
|
Py_ssize_t pos{0};
|
|
while (PyDict_Next(o, &pos, &key, &value)) {
|
|
if (!PyUnicode_Check(key)) {
|
|
throw std::invalid_argument("Dictionary keys must be strings");
|
|
}
|
|
|
|
const char *k = PyUnicode_AsUTF8(key);
|
|
|
|
if (!k) {
|
|
PyErr_Clear();
|
|
throw std::bad_alloc{};
|
|
}
|
|
|
|
MgpUniquePtr<mgp_value> v{PyObjectToMgpValue(value, memory), mgp_value_destroy};
|
|
|
|
if (const auto err = mgp_map_insert(map.get(), k, v.get()); err == mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE) {
|
|
throw std::bad_alloc{};
|
|
} else if (err != mgp_error::MGP_ERROR_NO_ERROR) {
|
|
throw std::runtime_error{"Unexpected error during inserting an item to mgp_map"};
|
|
}
|
|
}
|
|
|
|
if (const auto err = mgp_value_make_map(map.get(), &mgp_v); err == mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE) {
|
|
throw std::bad_alloc{};
|
|
} else if (err != mgp_error::MGP_ERROR_NO_ERROR) {
|
|
throw std::runtime_error{"Unexpected error during creating mgp_value"};
|
|
}
|
|
static_cast<void>(map.release());
|
|
} else if (Py_TYPE(o) == &PyEdgeType) {
|
|
MgpUniquePtr<mgp_edge> e{nullptr, mgp_edge_destroy};
|
|
// Copy the edge and pass the ownership to the created mgp_value.
|
|
|
|
if (const auto err = CreateMgpObject(e, mgp_edge_copy, reinterpret_cast<PyEdge *>(o)->edge, memory);
|
|
err == mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE) {
|
|
throw std::bad_alloc{};
|
|
} else if (err != mgp_error::MGP_ERROR_NO_ERROR) {
|
|
throw std::runtime_error{"Unexpected error during copying mgp_edge"};
|
|
}
|
|
if (const auto err = mgp_value_make_edge(e.get(), &mgp_v); err == mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE) {
|
|
throw std::bad_alloc{};
|
|
} else if (err != mgp_error::MGP_ERROR_NO_ERROR) {
|
|
throw std::runtime_error{"Unexpected error during copying mgp_edge"};
|
|
}
|
|
static_cast<void>(e.release());
|
|
} else if (Py_TYPE(o) == &PyPathType) {
|
|
MgpUniquePtr<mgp_path> p{nullptr, mgp_path_destroy};
|
|
// Copy the edge and pass the ownership to the created mgp_value.
|
|
|
|
if (const auto err = CreateMgpObject(p, mgp_path_copy, reinterpret_cast<PyPath *>(o)->path, memory);
|
|
err == mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE) {
|
|
throw std::bad_alloc{};
|
|
} else if (err != mgp_error::MGP_ERROR_NO_ERROR) {
|
|
throw std::runtime_error{"Unexpected error during copying mgp_path"};
|
|
}
|
|
if (const auto err = mgp_value_make_path(p.get(), &mgp_v); err == mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE) {
|
|
throw std::bad_alloc{};
|
|
} else if (err != mgp_error::MGP_ERROR_NO_ERROR) {
|
|
throw std::runtime_error{"Unexpected error during copying mgp_path"};
|
|
}
|
|
static_cast<void>(p.release());
|
|
} else if (Py_TYPE(o) == &PyVertexType) {
|
|
MgpUniquePtr<mgp_vertex> v{nullptr, mgp_vertex_destroy};
|
|
// Copy the edge and pass the ownership to the created mgp_value.
|
|
|
|
if (const auto err = CreateMgpObject(v, mgp_vertex_copy, reinterpret_cast<PyVertex *>(o)->vertex, memory);
|
|
err == mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE) {
|
|
throw std::bad_alloc{};
|
|
} else if (err != mgp_error::MGP_ERROR_NO_ERROR) {
|
|
throw std::runtime_error{"Unexpected error during copying mgp_vertex"};
|
|
}
|
|
if (const auto err = mgp_value_make_vertex(v.get(), &mgp_v); err == mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE) {
|
|
throw std::bad_alloc{};
|
|
} else if (err != mgp_error::MGP_ERROR_NO_ERROR) {
|
|
throw std::runtime_error{"Unexpected error during copying mgp_vertex"};
|
|
}
|
|
static_cast<void>(v.release());
|
|
} else if (is_mgp_instance(o, "Edge")) {
|
|
py::Object edge(PyObject_GetAttrString(o, "_edge"));
|
|
if (!edge) {
|
|
PyErr_Clear();
|
|
throw std::invalid_argument("'mgp.Edge' is missing '_edge' attribute");
|
|
}
|
|
return PyObjectToMgpValue(edge.Ptr(), memory);
|
|
} else if (is_mgp_instance(o, "Vertex")) {
|
|
py::Object vertex(PyObject_GetAttrString(o, "_vertex"));
|
|
if (!vertex) {
|
|
PyErr_Clear();
|
|
throw std::invalid_argument("'mgp.Vertex' is missing '_vertex' attribute");
|
|
}
|
|
return PyObjectToMgpValue(vertex.Ptr(), memory);
|
|
} else if (is_mgp_instance(o, "Path")) {
|
|
py::Object path(PyObject_GetAttrString(o, "_path"));
|
|
if (!path) {
|
|
PyErr_Clear();
|
|
throw std::invalid_argument("'mgp.Path' is missing '_path' attribute");
|
|
}
|
|
return PyObjectToMgpValue(path.Ptr(), memory);
|
|
} else if (PyDate_CheckExact(o)) {
|
|
mgp_date_parameters parameters{
|
|
.year = PyDateTime_GET_YEAR(o), // NOLINT(cppcoreguidelines-pro-type-cstyle-cast,hicpp-signed-bitwise)
|
|
.month = PyDateTime_GET_MONTH(o), // NOLINT(cppcoreguidelines-pro-type-cstyle-cast,hicpp-signed-bitwise)
|
|
.day = PyDateTime_GET_DAY(o)}; // NOLINT(cppcoreguidelines-pro-type-cstyle-cast,hicpp-signed-bitwise)
|
|
MgpUniquePtr<mgp_date> date{nullptr, mgp_date_destroy};
|
|
|
|
if (const auto err = CreateMgpObject(date, mgp_date_from_parameters, ¶meters, memory);
|
|
err == mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE) {
|
|
throw std::bad_alloc{};
|
|
} else if (err != mgp_error::MGP_ERROR_NO_ERROR) {
|
|
throw std::runtime_error{"Unexpected error while creating mgp_date"};
|
|
}
|
|
if (const auto err = mgp_value_make_date(date.get(), &mgp_v); err == mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE) {
|
|
throw std::bad_alloc{};
|
|
} else if (err != mgp_error::MGP_ERROR_NO_ERROR) {
|
|
throw std::runtime_error{"Unexpected error while creating mgp_value"};
|
|
}
|
|
static_cast<void>(date.release());
|
|
} else if (PyTime_CheckExact(o)) {
|
|
mgp_local_time_parameters parameters{
|
|
.hour = PyDateTime_TIME_GET_HOUR(o), // NOLINT(cppcoreguidelines-pro-type-cstyle-cast,hicpp-signed-bitwise)
|
|
.minute = PyDateTime_TIME_GET_MINUTE(o), // NOLINT(cppcoreguidelines-pro-type-cstyle-cast,hicpp-signed-bitwise)
|
|
.second = PyDateTime_TIME_GET_SECOND(o), // NOLINT(cppcoreguidelines-pro-type-cstyle-cast,hicpp-signed-bitwise)
|
|
.millisecond =
|
|
PyDateTime_TIME_GET_MICROSECOND(o) / // NOLINT(cppcoreguidelines-pro-type-cstyle-cast,hicpp-signed-bitwise)
|
|
1000,
|
|
.microsecond =
|
|
PyDateTime_TIME_GET_MICROSECOND(o) % // NOLINT(cppcoreguidelines-pro-type-cstyle-cast,hicpp-signed-bitwise)
|
|
1000};
|
|
MgpUniquePtr<mgp_local_time> local_time{nullptr, mgp_local_time_destroy};
|
|
|
|
if (const auto err = CreateMgpObject(local_time, mgp_local_time_from_parameters, ¶meters, memory);
|
|
err == mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE) {
|
|
throw std::bad_alloc{};
|
|
} else if (err != mgp_error::MGP_ERROR_NO_ERROR) {
|
|
throw std::runtime_error{"Unexpected error while creating mgp_local_time"};
|
|
}
|
|
if (const auto err = mgp_value_make_local_time(local_time.get(), &mgp_v);
|
|
err == mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE) {
|
|
throw std::bad_alloc{};
|
|
} else if (err != mgp_error::MGP_ERROR_NO_ERROR) {
|
|
throw std::runtime_error{"Unexpected error while creating mgp_value"};
|
|
}
|
|
static_cast<void>(local_time.release());
|
|
} else if (PyDateTime_CheckExact(o)) {
|
|
mgp_date_parameters date_parameters{
|
|
.year = PyDateTime_GET_YEAR(o), // NOLINT(cppcoreguidelines-pro-type-cstyle-cast,hicpp-signed-bitwise)
|
|
.month = PyDateTime_GET_MONTH(o), // NOLINT(cppcoreguidelines-pro-type-cstyle-cast,hicpp-signed-bitwise)
|
|
.day = PyDateTime_GET_DAY(o)}; // NOLINT(cppcoreguidelines-pro-type-cstyle-cast,hicpp-signed-bitwise)
|
|
mgp_local_time_parameters local_time_parameters{
|
|
.hour = PyDateTime_DATE_GET_HOUR(o), // NOLINT(cppcoreguidelines-pro-type-cstyle-cast,hicpp-signed-bitwise)
|
|
.minute = PyDateTime_DATE_GET_MINUTE(o), // NOLINT(cppcoreguidelines-pro-type-cstyle-cast,hicpp-signed-bitwise)
|
|
.second = PyDateTime_DATE_GET_SECOND(o), // NOLINT(cppcoreguidelines-pro-type-cstyle-cast,hicpp-signed-bitwise)
|
|
.millisecond =
|
|
PyDateTime_DATE_GET_MICROSECOND(o) / // NOLINT(cppcoreguidelines-pro-type-cstyle-cast,hicpp-signed-bitwise)
|
|
1000,
|
|
.microsecond =
|
|
PyDateTime_DATE_GET_MICROSECOND(o) % // NOLINT(cppcoreguidelines-pro-type-cstyle-cast,hicpp-signed-bitwise)
|
|
1000};
|
|
|
|
mgp_local_date_time_parameters parameters{&date_parameters, &local_time_parameters};
|
|
|
|
MgpUniquePtr<mgp_local_date_time> local_date_time{nullptr, mgp_local_date_time_destroy};
|
|
|
|
if (const auto err = CreateMgpObject(local_date_time, mgp_local_date_time_from_parameters, ¶meters, memory);
|
|
err == mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE) {
|
|
throw std::bad_alloc{};
|
|
} else if (err != mgp_error::MGP_ERROR_NO_ERROR) {
|
|
throw std::runtime_error{"Unexpected error while creating mgp_local_date_time"};
|
|
}
|
|
if (const auto err = mgp_value_make_local_date_time(local_date_time.get(), &mgp_v);
|
|
err == mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE) {
|
|
throw std::bad_alloc{};
|
|
} else if (err != mgp_error::MGP_ERROR_NO_ERROR) {
|
|
throw std::runtime_error{"Unexpected error while creating mgp_value"};
|
|
}
|
|
static_cast<void>(local_date_time.release());
|
|
} else if (PyDelta_CheckExact(o)) {
|
|
static constexpr int64_t microseconds_in_days =
|
|
static_cast<std::chrono::microseconds>(std::chrono::days{1}).count();
|
|
const auto days =
|
|
PyDateTime_DELTA_GET_DAYS(o); // NOLINT(cppcoreguidelines-pro-type-cstyle-cast,hicpp-signed-bitwise)
|
|
auto microseconds =
|
|
std::abs(days) * microseconds_in_days +
|
|
PyDateTime_DELTA_GET_SECONDS(o) * 1000 * // NOLINT(cppcoreguidelines-pro-type-cstyle-cast,hicpp-signed-bitwise)
|
|
1000 +
|
|
PyDateTime_DELTA_GET_MICROSECONDS(o); // NOLINT(cppcoreguidelines-pro-type-cstyle-cast,hicpp-signed-bitwise)
|
|
microseconds *= days < 0 ? -1 : 1;
|
|
|
|
MgpUniquePtr<mgp_duration> duration{nullptr, mgp_duration_destroy};
|
|
|
|
if (const auto err = CreateMgpObject(duration, mgp_duration_from_microseconds, microseconds, memory);
|
|
err == mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE) {
|
|
throw std::bad_alloc{};
|
|
} else if (err != mgp_error::MGP_ERROR_NO_ERROR) {
|
|
throw std::runtime_error{"Unexpected error while creating mgp_duration"};
|
|
}
|
|
if (const auto err = mgp_value_make_duration(duration.get(), &mgp_v);
|
|
err == mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE) {
|
|
throw std::bad_alloc{};
|
|
} else if (err != mgp_error::MGP_ERROR_NO_ERROR) {
|
|
throw std::runtime_error{"Unexpected error while creating mgp_value"};
|
|
}
|
|
static_cast<void>(duration.release());
|
|
} else {
|
|
throw std::invalid_argument("Unsupported PyObject conversion");
|
|
}
|
|
|
|
if (last_error == mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE) {
|
|
throw std::bad_alloc{};
|
|
}
|
|
if (last_error != mgp_error::MGP_ERROR_NO_ERROR) {
|
|
throw std::runtime_error{"Unexpected error while creating mgp_value"};
|
|
}
|
|
|
|
return mgp_v;
|
|
}
|
|
|
|
PyObject *PyGraphCreateEdge(PyGraph *self, PyObject *args) {
|
|
MG_ASSERT(PyGraphIsValidImpl(*self));
|
|
MG_ASSERT(self->memory);
|
|
PyVertex *from{nullptr};
|
|
PyVertex *to{nullptr};
|
|
const char *edge_type{nullptr};
|
|
if (!PyArg_ParseTuple(args, "O!O!s", &PyVertexType, &from, &PyVertexType, &to, &edge_type)) {
|
|
return nullptr;
|
|
}
|
|
MgpUniquePtr<mgp_edge> new_edge{nullptr, mgp_edge_destroy};
|
|
if (RaiseExceptionFromErrorCode(CreateMgpObject(new_edge, mgp_graph_create_edge, self->graph, from->vertex,
|
|
to->vertex, mgp_edge_type{edge_type}, self->memory))) {
|
|
return nullptr;
|
|
}
|
|
auto *py_edge = MakePyEdgeWithoutCopy(*new_edge, self);
|
|
if (py_edge != nullptr) {
|
|
static_cast<void>(new_edge.release());
|
|
}
|
|
return py_edge;
|
|
}
|
|
|
|
PyObject *PyGraphDeleteVertex(PyGraph *self, PyObject *args) {
|
|
MG_ASSERT(PyGraphIsValidImpl(*self));
|
|
MG_ASSERT(self->memory);
|
|
PyVertex *vertex{nullptr};
|
|
if (!PyArg_ParseTuple(args, "O!", &PyVertexType, &vertex)) {
|
|
return nullptr;
|
|
}
|
|
if (RaiseExceptionFromErrorCode(mgp_graph_delete_vertex(self->graph, vertex->vertex))) {
|
|
return nullptr;
|
|
}
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
PyObject *PyGraphDetachDeleteVertex(PyGraph *self, PyObject *args) {
|
|
MG_ASSERT(PyGraphIsValidImpl(*self));
|
|
MG_ASSERT(self->memory);
|
|
PyVertex *vertex{nullptr};
|
|
if (!PyArg_ParseTuple(args, "O!", &PyVertexType, &vertex)) {
|
|
return nullptr;
|
|
}
|
|
if (RaiseExceptionFromErrorCode(mgp_graph_detach_delete_vertex(self->graph, vertex->vertex))) {
|
|
return nullptr;
|
|
}
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
PyObject *PyGraphDeleteEdge(PyGraph *self, PyObject *args) {
|
|
MG_ASSERT(PyGraphIsValidImpl(*self));
|
|
MG_ASSERT(self->memory);
|
|
PyEdge *edge{nullptr};
|
|
if (!PyArg_ParseTuple(args, "O!", &PyEdgeType, &edge)) {
|
|
return nullptr;
|
|
}
|
|
if (RaiseExceptionFromErrorCode(mgp_graph_delete_edge(self->graph, edge->edge))) {
|
|
return nullptr;
|
|
}
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
} // namespace memgraph::query::procedure
|