This is an automated email from the ASF dual-hosted git repository.
isapego pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push:
new 496fe683cd IGNITE-22353 Basic Python DB API Driver (#4075)
496fe683cd is described below
commit 496fe683cd04b5c0f7395d542eaf8fff411f3349
Author: Igor Sapego <[email protected]>
AuthorDate: Fri Jul 12 14:43:31 2024 +0400
IGNITE-22353 Basic Python DB API Driver (#4075)
---
modules/platforms/cpp/CMakeLists.txt | 6 +-
.../cpp/ignite/odbc/config/configuration.h | 35 ++++
modules/platforms/python/.gitignore | 13 ++
modules/platforms/python/CMakeLists.txt | 54 ++++++
modules/platforms/python/LICENSE | 14 ++
modules/platforms/python/MANIFEST.in | 7 +
modules/platforms/python/NOTICE | 5 +
modules/platforms/python/README.md | 48 +++++
modules/platforms/python/cpp_module/CMakeLists.txt | 56 ++++++
modules/platforms/python/cpp_module/module.cpp | 195 +++++++++++++++++++++
modules/platforms/python/cpp_module/module.h | 24 +++
.../platforms/python/cpp_module/py_connection.cpp | 109 ++++++++++++
.../platforms/python/cpp_module/py_connection.h | 70 ++++++++
modules/platforms/python/pyignite3/__init__.py | 149 ++++++++++++++++
modules/platforms/python/requirements/install.txt | 3 +
modules/platforms/python/requirements/tests.txt | 7 +
modules/platforms/python/setup.py | 158 +++++++++++++++++
modules/platforms/python/tests/__init__.py | 14 ++
modules/platforms/python/tests/conftest.py | 18 ++
modules/platforms/python/tests/test_connect.py | 33 ++++
modules/platforms/python/tests/util.py | 155 ++++++++++++++++
21 files changed, 1171 insertions(+), 2 deletions(-)
diff --git a/modules/platforms/cpp/CMakeLists.txt
b/modules/platforms/cpp/CMakeLists.txt
index 4277aac276..fb25a9a79e 100644
--- a/modules/platforms/cpp/CMakeLists.txt
+++ b/modules/platforms/cpp/CMakeLists.txt
@@ -32,7 +32,7 @@ option(ENABLE_UB_SANITIZER "If undefined behavior sanitizer
is enabled" OFF)
option(WARNINGS_AS_ERRORS "Treat warning as errors" OFF)
option(INSTALL_IGNITE_FILES "Install Ignite files" ON)
-set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
+list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
list(APPEND CMAKE_MODULE_PATH ${CMAKE_BINARY_DIR})
list(APPEND CMAKE_PREFIX_PATH ${CMAKE_BINARY_DIR})
@@ -65,7 +65,9 @@ else()
endif()
set(IGNITE_INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR}/ignite)
-message(STATUS "CMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX}")
+if(${INSTALL_IGNITE_FILES})
+ message(STATUS "CMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX}")
+endif()
message(STATUS "IGNITE_INCLUDEDIR=${IGNITE_INCLUDEDIR}")
include(ignite_install_headers)
diff --git a/modules/platforms/cpp/ignite/odbc/config/configuration.h
b/modules/platforms/cpp/ignite/odbc/config/configuration.h
index 5dc663921c..0cfd2af2f3 100644
--- a/modules/platforms/cpp/ignite/odbc/config/configuration.h
+++ b/modules/platforms/cpp/ignite/odbc/config/configuration.h
@@ -68,6 +68,13 @@ public:
*/
[[nodiscard]] const value_with_default<std::vector<end_point>>
&get_address() const { return m_end_points; }
+ /**
+ * Set addresses.
+ *
+ * @param addr Addresses.
+ */
+ void set_address(std::string addr) { m_end_points = {parse_address(addr),
true}; }
+
/**
* Get fetch results page size.
*
@@ -75,6 +82,13 @@ public:
*/
[[nodiscard]] const value_with_default<std::int32_t> &get_page_size()
const { return m_page_size; }
+ /**
+ * Set fetch results page size.
+ *
+ * @param page_size Fetch results page size.
+ */
+ void set_page_size(std::int32_t page_size) { m_page_size = {page_size,
true}; }
+
/**
* Get schema.
*
@@ -82,6 +96,13 @@ public:
*/
[[nodiscard]] const value_with_default<std::string> &get_schema() const {
return m_schema; }
+ /**
+ * Set schema.
+ *
+ * @param schema Schema.
+ */
+ void set_schema(std::string schema) { m_schema = {std::move(schema),
true}; }
+
/**
* Get authentication type.
*
@@ -96,6 +117,13 @@ public:
*/
[[nodiscard]] const value_with_default<std::string> &get_auth_identity()
const { return m_auth_identity; };
+ /**
+ * Set identity.
+ *
+ * @param identity Identity.
+ */
+ void set_auth_identity(std::string identity) { m_auth_identity =
{std::move(identity), true}; }
+
/**
* Get secret.
*
@@ -103,6 +131,13 @@ public:
*/
[[nodiscard]] const value_with_default<std::string> &get_auth_secret()
const { return m_auth_secret; };
+ /**
+ * Set secret.
+ *
+ * @param secret Secret.
+ */
+ void set_auth_secret(std::string secret) { m_auth_secret =
{std::move(secret), true}; }
+
/**
* Get Timezone.
*
diff --git a/modules/platforms/python/.gitignore
b/modules/platforms/python/.gitignore
new file mode 100644
index 0000000000..b5aad1f951
--- /dev/null
+++ b/modules/platforms/python/.gitignore
@@ -0,0 +1,13 @@
+.idea
+.benchmarks
+.vscode
+.eggs
+*.egg-info
+.pytest_cache
+*.so
+*.pyd
+build
+distr
+docs/generated
+__pycache__
+venv
\ No newline at end of file
diff --git a/modules/platforms/python/CMakeLists.txt
b/modules/platforms/python/CMakeLists.txt
new file mode 100644
index 0000000000..1be294ce4a
--- /dev/null
+++ b/modules/platforms/python/CMakeLists.txt
@@ -0,0 +1,54 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+cmake_minimum_required(VERSION 3.18)
+project(pyignite3 VERSION 3 LANGUAGES CXX)
+
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+# Dealing with Python native libraries
+set(Python_FIND_VIRTUALENV FIRST)
+find_package(Python3 COMPONENTS Interpreter Development)
+
+message("Python3_FOUND:${Python3_FOUND}")
+message("Python3_VERSION:${Python3_VERSION}")
+message("Python3_Development_FOUND:${Python3_Development_FOUND}")
+message("Python3_LIBRARIES:${Python3_LIBRARIES}")
+message("EXTENSION_FILENAME:${EXTENSION_FILENAME}")
+
+# Dealing with Ignite C++ libraries
+set(ENABLE_ODBC ON)
+set(ENABLE_CLIENT OFF)
+set(ENABLE_TESTS OFF)
+set(INSTALL_IGNITE_FILES OFF)
+set(ENABLE_ADDRESS_SANITIZER OFF)
+set(ENABLE_UB_SANITIZER OFF)
+set(WARNINGS_AS_ERRORS OFF)
+
+set(IGNITE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../cpp)
+set(IGNITE_BIN_DIR ${CMAKE_CURRENT_BINARY_DIR})
+set(IGNITE_CMAKE_MODULE_PATH ${IGNITE_DIR}/cmake)
+
+list(APPEND CMAKE_MODULE_PATH ${IGNITE_CMAKE_MODULE_PATH})
+list(APPEND CMAKE_MODULE_PATH ${CMAKE_BINARY_DIR})
+list(APPEND CMAKE_PREFIX_PATH ${CMAKE_BINARY_DIR})
+
+include_directories(${IGNITE_DIR})
+
+add_subdirectory(${IGNITE_DIR} ${IGNITE_BIN_DIR}/ignite)
+add_subdirectory(cpp_module)
diff --git a/modules/platforms/python/LICENSE b/modules/platforms/python/LICENSE
new file mode 100644
index 0000000000..e95c88676b
--- /dev/null
+++ b/modules/platforms/python/LICENSE
@@ -0,0 +1,14 @@
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
\ No newline at end of file
diff --git a/modules/platforms/python/MANIFEST.in
b/modules/platforms/python/MANIFEST.in
new file mode 100644
index 0000000000..95773bc25f
--- /dev/null
+++ b/modules/platforms/python/MANIFEST.in
@@ -0,0 +1,7 @@
+graft requirements
+graft tests
+global-exclude *.py[cod]
+global-exclude *__pycache__*
+include README.md
+include LICENSE
+include NOTICE
diff --git a/modules/platforms/python/NOTICE b/modules/platforms/python/NOTICE
new file mode 100644
index 0000000000..18b525f0e0
--- /dev/null
+++ b/modules/platforms/python/NOTICE
@@ -0,0 +1,5 @@
+Apache Ignite 3 DB API Driver
+Copyright 2024 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
diff --git a/modules/platforms/python/README.md
b/modules/platforms/python/README.md
new file mode 100644
index 0000000000..8ec82457b2
--- /dev/null
+++ b/modules/platforms/python/README.md
@@ -0,0 +1,48 @@
+# pyignite3
+Apache Ignite 3 DB API Driver.
+
+## Prerequisites
+
+- Python 3.7 or above (3.7, 3.8, 3.9 and 3.10 are tested),
+- Access to Ignite 3 node, local or remote.
+
+## Installation
+
+### From sources
+This way is more suitable for developers or if you install client from zip
archive.
+1. Download and/or unzip Ignite 3 DB API Driver sources to `pyignite3_path`
+2. Go to `pyignite3_path` folder
+3. Execute `pip install -e .`
+
+```bash
+$ cd <pyignite3_path>
+$ pip install -e .
+```
+
+This will install the repository version of `pyignite3` into your environment
+in so-called “develop” or “editable” mode. You may read more about
+[editable
installs](https://pip.pypa.io/en/stable/reference/pip_install/#editable-installs)
+in the `pip` manual.
+
+Then run through the contents of `requirements` folder to install
+the additional requirements into your working Python environment using
+```
+$ pip install -r requirements/<your task>.txt
+```
+
+You may also want to consult the `setuptools` manual about using `setup.py`.
+
+## Testing
+*NB!* It is recommended installing `pyignite3` in development mode.
+Refer to [this section](#from-sources) for instructions.
+
+Do not forget to install test requirements:
+```bash
+$ pip install -r requirements/install.txt -r requirements/tests.txt
+```
+
+### Run basic tests
+Running tests themselves:
+```bash
+$ pytest
+```
diff --git a/modules/platforms/python/cpp_module/CMakeLists.txt
b/modules/platforms/python/cpp_module/CMakeLists.txt
new file mode 100644
index 0000000000..1df497efb8
--- /dev/null
+++ b/modules/platforms/python/cpp_module/CMakeLists.txt
@@ -0,0 +1,56 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+project(_pyignite3_extension)
+
+set(TARGET ${PROJECT_NAME})
+
+find_package(ODBC REQUIRED)
+
+set(SOURCES
+ py_connection.cpp
+ module.cpp
+)
+
+set(LIBRARIES
+ ignite-common
+ ignite-tuple
+ ignite-network
+ ignite-protocol
+ ignite3-odbc-obj
+)
+
+add_library(${TARGET} SHARED ${SOURCES})
+
+target_include_directories(${TARGET} PUBLIC
+ ${Python3_INCLUDE_DIRS}
+ ${ODBC_INCLUDE_DIRS}
+)
+
+if(NOT DEFINED EXTENSION_FILENAME)
+ set(EXTENSION_FILENAME ${TARGET})
+endif()
+
+target_link_libraries(${TARGET} ${LIBRARIES})
+set_target_properties(${TARGET} PROPERTIES OUTPUT_NAME ${EXTENSION_FILENAME})
+
+if (WIN32)
+ add_compile_definitions(NOMINMAX)
+ set_target_properties(${TARGET} PROPERTIES SUFFIX ".pyd")
+ target_link_libraries(${TARGET} ${Python3_LIBRARIES})
+ remove_definitions(-DUNICODE=1)
+endif()
diff --git a/modules/platforms/python/cpp_module/module.cpp
b/modules/platforms/python/cpp_module/module.cpp
new file mode 100644
index 0000000000..4b7bd1abac
--- /dev/null
+++ b/modules/platforms/python/cpp_module/module.cpp
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "module.h"
+#include "py_connection.h"
+
+#include <ignite/odbc/sql_environment.h>
+#include <ignite/odbc/sql_connection.h>
+
+#include <memory>
+#include <cmath>
+
+#include <Python.h>
+
+
+PyObject* connect(PyObject* self, PyObject *args, PyObject* kwargs);
+
+static PyMethodDef methods[] = {
+ {"connect", (PyCFunction) connect, METH_VARARGS | METH_KEYWORDS, nullptr},
+ {nullptr, nullptr, 0, nullptr} /* Sentinel */
+};
+
+static struct PyModuleDef module_def = {
+ PyModuleDef_HEAD_INIT,
+ MODULE_NAME,
+ nullptr, /* m_doc */
+ -1, /* m_size */
+ methods, /* m_methods */
+ nullptr, /* m_slots */
+ nullptr, /* m_traverse */
+ nullptr, /* m_clear */
+ nullptr, /* m_free */
+};
+
+PyMODINIT_FUNC PyInit__pyignite3_extension(void) { //
NOLINT(*-reserved-identifier)
+ PyObject* mod;
+
+ mod = PyModule_Create(&module_def);
+ if (mod == nullptr)
+ return nullptr;
+
+ if (prepare_py_connection_type())
+ return nullptr;
+
+ if (register_py_connection_type(mod))
+ return nullptr;
+
+ return mod;
+}
+
+bool check_errors(ignite::diagnosable& diag) {
+ auto &records = diag.get_diagnostic_records();
+ if (records.is_successful())
+ return true;
+
+ std::string err_msg;
+ switch (records.get_return_code()) {
+ case SQL_INVALID_HANDLE:
+ err_msg = "Invalid object handle";
+ break;
+
+ case SQL_NO_DATA:
+ err_msg = "No data available";
+ break;
+
+ case SQL_ERROR:
+ auto record = records.get_status_record(1);
+ err_msg = record.get_message_text();
+ break;
+ }
+
+ // TODO: IGNITE-22226 Set a proper error here, not a standard one.
+ PyErr_SetString(PyExc_RuntimeError, err_msg.c_str());
+
+ return false;
+}
+
+static PyObject* make_connection(std::unique_ptr<ignite::sql_environment> env,
+ std::unique_ptr<ignite::sql_connection> conn) {
+ auto pyignite3_mod = PyImport_ImportModule("pyignite3");
+
+ if (!pyignite3_mod)
+ return nullptr;
+
+ auto conn_class = PyObject_GetAttrString(pyignite3_mod, "Connection");
+ Py_DECREF(pyignite3_mod);
+
+ if (!conn_class)
+ return nullptr;
+
+ auto args = PyTuple_New(0);
+ auto kwargs = Py_BuildValue("{}");
+ PyObject* conn_obj = PyObject_Call(conn_class, args, kwargs);
+ Py_DECREF(conn_class);
+ Py_DECREF(args);
+ Py_DECREF(kwargs);
+
+ if (!conn_obj)
+ return nullptr;
+
+ auto py_conn = make_py_connection(std::move(env), std::move(conn));
+ if (!py_conn)
+ return nullptr;
+
+ auto res = PyObject_SetAttrString(conn_obj, "_py_connection",
(PyObject*)py_conn);
+ if (res)
+ return nullptr;
+
+ return conn_obj;
+}
+
+static PyObject* connect(PyObject* self, PyObject* args, PyObject* kwargs) {
+ static char *kwlist[] = {
+ "address",
+ "identity",
+ "secret",
+ "schema",
+ "timezone",
+ "page_size",
+ "timeout",
+ nullptr
+ };
+
+ const char* address = nullptr;
+ const char* identity = nullptr;
+ const char* secret = nullptr;
+ const char* schema = nullptr;
+ const char* timezone = nullptr;
+ double timeout = 0.0;
+ int page_size = 0;
+
+ int parsed = PyArg_ParseTupleAndKeywords(
+ args, kwargs, "s|ssssdi", kwlist, &address, &identity, &secret,
&schema, &timezone, &timeout, &page_size);
+
+ if (!parsed)
+ return nullptr;
+
+ using namespace ignite;
+
+ auto sql_env = std::make_unique<sql_environment>();
+
+ std::unique_ptr<sql_connection> sql_conn{sql_env->create_connection()};
+ if (!check_errors(*sql_env))
+ return nullptr;
+
+ configuration cfg;
+ cfg.set_address(address);
+
+ if (schema)
+ cfg.set_schema(schema);
+
+ if (identity)
+ cfg.set_auth_identity(identity);
+
+ if (secret)
+ cfg.set_auth_secret(secret);
+
+ if (page_size)
+ cfg.set_page_size(std::int32_t(page_size));
+
+ std::int32_t s_timeout = std::lround(timeout);
+ if (s_timeout)
+ {
+ void* ptr_timeout = (void*)(ptrdiff_t(s_timeout));
+ sql_conn->set_attribute(SQL_ATTR_CONNECTION_TIMEOUT, ptr_timeout, 0);
+ if (!check_errors(*sql_conn))
+ return nullptr;
+
+ sql_conn->set_attribute(SQL_ATTR_LOGIN_TIMEOUT, ptr_timeout, 0);
+ if (!check_errors(*sql_conn))
+ return nullptr;
+ }
+
+ sql_conn->establish(cfg);
+ if (!check_errors(*sql_conn))
+ return nullptr;
+
+ return make_connection(std::move(sql_env), std::move(sql_conn));
+}
+
+
diff --git a/modules/platforms/python/cpp_module/module.h
b/modules/platforms/python/cpp_module/module.h
new file mode 100644
index 0000000000..4832724e11
--- /dev/null
+++ b/modules/platforms/python/cpp_module/module.h
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define MODULE_NAME "_pyignite3_extension"
+
+namespace ignite {
+class diagnosable;
+}
+
+bool check_errors(ignite::diagnosable& diag);
diff --git a/modules/platforms/python/cpp_module/py_connection.cpp
b/modules/platforms/python/cpp_module/py_connection.cpp
new file mode 100644
index 0000000000..96bb5f1259
--- /dev/null
+++ b/modules/platforms/python/cpp_module/py_connection.cpp
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <ignite/odbc/sql_environment.h>
+#include <ignite/odbc/sql_connection.h>
+
+#include <ignite/common/detail/config.h>
+
+#include "module.h"
+#include "py_connection.h"
+
+#include <Python.h>
+
+int py_connection_init(py_connection *self, PyObject *args, PyObject *kwds)
+{
+ UNUSED_VALUE args;
+ UNUSED_VALUE kwds;
+
+ self->m_env = nullptr;
+ self->m_conn = nullptr;
+
+ return 0;
+}
+
+void py_connection_dealloc(py_connection *self)
+{
+ delete self->m_conn;
+ delete self->m_env;
+
+ self->m_conn = nullptr;
+ self->m_env = nullptr;
+
+ Py_TYPE(self)->tp_free(self);
+}
+
+static PyObject* py_connection_close(py_connection* self, PyObject*)
+{
+ if (self->m_conn) {
+ self->m_conn->release();
+ if (!check_errors(*self->m_conn))
+ return nullptr;
+
+ delete self->m_conn;
+ self->m_conn = nullptr;
+
+ delete self->m_env;
+ self->m_env = nullptr;
+ }
+
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
+static PyTypeObject py_connection_type = {
+ PyVarObject_HEAD_INIT(nullptr, 0)
+ MODULE_NAME "." PY_CONNECTION_CLASS_NAME
+};
+
+static struct PyMethodDef py_connection_methods[] = {
+ {"close", (PyCFunction)py_connection_close, METH_NOARGS, nullptr},
+ {nullptr, nullptr, 0, nullptr}
+};
+
+int prepare_py_connection_type() {
+ py_connection_type.tp_new = PyType_GenericNew;
+ py_connection_type.tp_basicsize=sizeof(py_connection);
+ py_connection_type.tp_dealloc=(destructor)py_connection_dealloc;
+ py_connection_type.tp_flags=Py_TPFLAGS_DEFAULT;
+ py_connection_type.tp_methods=py_connection_methods;
+ py_connection_type.tp_init=(initproc)py_connection_init;
+
+ return PyType_Ready(&py_connection_type);
+}
+
+int register_py_connection_type(PyObject* mod) {
+ return PyModule_AddObjectRef(mod, PY_CONNECTION_CLASS_NAME, (PyObject
*)&py_connection_type);
+}
+
+py_connection *make_py_connection(std::unique_ptr<ignite::sql_environment> env,
+ std::unique_ptr<ignite::sql_connection> conn) {
+ auto args = PyTuple_New(0);
+ auto kwargs = Py_BuildValue("{}");
+ PyObject* py_conn_obj = PyObject_Call((PyObject*)&py_connection_type,
args, kwargs);
+ Py_DECREF(args);
+ Py_DECREF(kwargs);
+
+ if (!py_conn_obj)
+ return nullptr;
+
+ auto typed_conn = reinterpret_cast<py_connection*>(py_conn_obj);
+ typed_conn->m_env = env.release();
+ typed_conn->m_conn = conn.release();
+
+ return typed_conn;
+}
diff --git a/modules/platforms/python/cpp_module/py_connection.h
b/modules/platforms/python/cpp_module/py_connection.h
new file mode 100644
index 0000000000..c71d1bcb98
--- /dev/null
+++ b/modules/platforms/python/cpp_module/py_connection.h
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <memory>
+
+#include <Python.h>
+
+#define PY_CONNECTION_CLASS_NAME "PyConnection"
+
+namespace ignite {
+class sql_environment;
+class sql_connection;
+}
+
+/**
+ * Connection Python object.
+ */
+struct py_connection {
+ PyObject_HEAD
+
+ /** Environment. */
+ ignite::sql_environment *m_env;
+
+ /** Connection. */
+ ignite::sql_connection *m_conn;
+};
+
+/**
+ * Connection init function.
+ */
+int py_connection_init(py_connection *self, PyObject *args, PyObject *kwds);
+
+/**
+ * Connection dealloc function.
+ */
+void py_connection_dealloc(py_connection *self);
+
+/**
+ * Create a new instance of py_connection python class.
+ *
+ * @param env Environment.
+ * @param conn Connection.
+ * @return A new class instance.
+ */
+py_connection* make_py_connection(std::unique_ptr<ignite::sql_environment> env,
+ std::unique_ptr<ignite::sql_connection> conn);
+
+/**
+ * Prepare PyConnection type for registration.
+ */
+int prepare_py_connection_type();
+
+/**
+ * Register PyConnection type within module.
+ */
+int register_py_connection_type(PyObject* mod);
\ No newline at end of file
diff --git a/modules/platforms/python/pyignite3/__init__.py
b/modules/platforms/python/pyignite3/__init__.py
new file mode 100644
index 0000000000..e2f01c0f1b
--- /dev/null
+++ b/modules/platforms/python/pyignite3/__init__.py
@@ -0,0 +1,149 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from pyignite3 import _pyignite3_extension
+
+__version__ = '3.0.0-beta2'
+
+# PEP 249 is supported
+apilevel = '2.0'
+
+# Threads may share the module, but not connections.
+threadsafety = 1
+
+# Parameter style is a question mark, e.g. '...WHERE name=?'
+paramstyle = 'qmark'
+
+
+class Cursor:
+ def callproc(self, *args):
+ # TODO: IGNITE-22226 Implement cursor support
+ raise NotSupportedError('Stored procedures are not supported')
+
+ def close(self):
+ # TODO: IGNITE-22226 Implement cursor support
+ raise NotSupportedError('Operation is not supported')
+
+ def execute(self, *args):
+ # TODO: IGNITE-22226 Implement cursor support
+ raise NotSupportedError('Operation is not supported')
+
+ def executemany(self, *args):
+ # TODO: IGNITE-22226 Implement cursor support
+ raise NotSupportedError('Operation is not supported')
+
+ def fetchone(self):
+ # TODO: IGNITE-22226 Implement cursor support
+ raise NotSupportedError('Operation is not supported')
+
+ def fetchmany(self):
+ # TODO: IGNITE-22226 Implement cursor support
+ raise NotSupportedError('Operation is not supported')
+
+ def fetchall(self):
+ # TODO: IGNITE-22226 Implement cursor support
+ raise NotSupportedError('Operation is not supported')
+
+ def nextset(self):
+ # TODO: IGNITE-22226 Implement cursor support
+ raise NotSupportedError('Operation is not supported')
+
+ def arraysize(self) -> int:
+ # TODO: IGNITE-22226 Implement cursor support
+ raise NotSupportedError('Operation is not supported')
+
+ def setinputsizes(self, *args):
+ # TODO: IGNITE-22226 Implement cursor support
+ raise NotSupportedError('Operation is not supported')
+
+ def setoutputsize(self, *args):
+ # TODO: IGNITE-22226 Implement cursor support
+ raise NotSupportedError('Operation is not supported')
+
+
+class Connection:
+ """
+ Connection class. Represents a single connection to the Ignite cluster.
+ """
+ def __init__(self):
+ self._py_connection = None
+
+ def close(self):
+ """
+ Close active connection.
+ Completes without errors on successfully closed connections.
+ """
+ if self._py_connection is not None:
+ self._py_connection.close()
+ self._py_connection = None
+
+ def commit(self):
+ # TODO: IGNITE-22226 Implement transaction support
+ raise NotSupportedError('Transactions are not supported')
+
+ def rollback(self):
+ # TODO: IGNITE-22226 Implement transaction support
+ raise NotSupportedError('Transactions are not supported')
+
+ def cursor(self) -> Cursor:
+ # TODO: IGNITE-22226 Implement cursor support
+ raise NotSupportedError('Operation is not supported')
+
+
+def connect(**kwargs) -> Connection:
+ """
+ Establish connection with the Ignite cluster.
+ """
+ return _pyignite3_extension.connect(**kwargs)
+
+
+class Error(Exception):
+ pass
+
+
+class Warning(Exception):
+ pass
+
+
+class InterfaceError(Error):
+ pass
+
+
+class DatabaseError(Error):
+ pass
+
+
+class InternalError(DatabaseError):
+ pass
+
+
+class OperationalError(DatabaseError):
+ pass
+
+
+class ProgrammingError(DatabaseError):
+ pass
+
+
+class IntegrityError(DatabaseError):
+ pass
+
+
+class DataError(DatabaseError):
+ pass
+
+
+class NotSupportedError(DatabaseError):
+ pass
diff --git a/modules/platforms/python/requirements/install.txt
b/modules/platforms/python/requirements/install.txt
new file mode 100644
index 0000000000..fc8879c325
--- /dev/null
+++ b/modules/platforms/python/requirements/install.txt
@@ -0,0 +1,3 @@
+# these pip packages are necessary for the pyignite3 to run
+
+attrs==23.1.0
\ No newline at end of file
diff --git a/modules/platforms/python/requirements/tests.txt
b/modules/platforms/python/requirements/tests.txt
new file mode 100644
index 0000000000..4b66fe8c12
--- /dev/null
+++ b/modules/platforms/python/requirements/tests.txt
@@ -0,0 +1,7 @@
+# these packages are used for testing
+
+pytest==6.2.5
+pytest-cov==2.11.1
+teamcity-messages==1.28
+psutil==5.8.0
+flake8==3.8.4
diff --git a/modules/platforms/python/setup.py
b/modules/platforms/python/setup.py
new file mode 100644
index 0000000000..9779d2e8b9
--- /dev/null
+++ b/modules/platforms/python/setup.py
@@ -0,0 +1,158 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import os
+import platform
+import re
+import subprocess
+import setuptools
+import sys
+import multiprocessing
+from pprint import pprint
+from setuptools.command.build_ext import build_ext
+from setuptools.extension import Extension
+
+PACKAGE_NAME = 'pyignite3'
+EXTENSION_NAME = 'pyignite3._pyignite3_extension'
+
+
+def is_a_requirement(req_line):
+ return not any([
+ req_line.startswith('#'),
+ req_line.startswith('-r'),
+ len(req_line) == 0,
+ ])
+
+
+install_requirements = []
+with open('requirements/install.txt', 'r', encoding='utf-8') as
requirements_file:
+ for line in requirements_file.readlines():
+ line = line.strip('\n')
+ if is_a_requirement(line):
+ install_requirements.append(line)
+
+with open('README.md', 'r', encoding='utf-8') as readme_file:
+ long_description = readme_file.read()
+
+version = None
+with open(PACKAGE_NAME + '/__init__.py', 'r') as fd:
+ version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]',
+ fd.read(), re.MULTILINE).group(1)
+ if not version:
+ raise RuntimeError('Cannot find version information')
+
+
+def _get_env_variable(name, default='OFF'):
+ if name not in os.environ.keys():
+ return default
+ return os.environ[name]
+
+
+# Command line flags forwarded to CMake (for debug purpose)
+cmake_cmd_args = []
+for f in sys.argv:
+ if f.startswith('-D'):
+ cmake_cmd_args.append(f)
+
+
+class CMakeExtension(Extension):
+ def __init__(self, name, cmake_lists_dir='.', sources=[], **kwa):
+ Extension.__init__(self, name, sources=sources, **kwa)
+ self.cmake_lists_dir = os.path.abspath(cmake_lists_dir)
+
+
+class CMakeBuild(build_ext):
+ def build_extensions(self):
+ try:
+ subprocess.check_output(['cmake', '--version'])
+ except OSError:
+ raise RuntimeError('Cannot find CMake executable')
+
+ for ext in self.extensions:
+ ext_dir =
os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name)))
+ cfg = 'Release'
+ ext_file =
os.path.splitext(self.get_ext_filename(self.get_ext_fullname(ext.name)))[0]
+
+ cmake_args = [
+ f'-DCMAKE_BUILD_TYPE={cfg}',
+ f'-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{cfg.upper()}={ext_dir}',
+
f'-DCMAKE_ARCHIVE_OUTPUT_DIRECTORY_{cfg.upper()}={self.build_temp}',
+ f'-DPYTHON_EXECUTABLE={sys.executable}',
+ f'-DEXTENSION_FILENAME={ext_file}',
+ ]
+
+ if platform.system() == 'Windows':
+ plat = ('x64' if platform.architecture()[0] == '64bit' else
'Win32')
+ cmake_args += [
+ '-DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS=TRUE',
+
f'-DCMAKE_RUNTIME_OUTPUT_DIRECTORY_{cfg.upper()}={ext_dir}',
+ ]
+ if self.compiler.compiler_type == 'msvc':
+ cmake_args += [
+ f'-DCMAKE_GENERATOR_PLATFORM={plat}',
+ ]
+ else:
+ raise RuntimeError('Only MSVC is supported for Windows
currently')
+
+ cmake_args += cmake_cmd_args
+
+ pprint(cmake_args)
+
+ if not os.path.exists(self.build_temp):
+ os.makedirs(self.build_temp)
+
+ cpu_count = multiprocessing.cpu_count()
+
+ # Config and build the extension
+ subprocess.check_call(['cmake', ext.cmake_lists_dir] + cmake_args,
cwd=self.build_temp)
+ subprocess.check_call(['cmake', '--build', '.', '-j',
str(cpu_count), '--config', cfg],
+ cwd=self.build_temp)
+
+
+def run_setup():
+ setuptools.setup(
+ name=PACKAGE_NAME,
+ version=version,
+ python_requires='>=3.8',
+ author='The Apache Software Foundation',
+ author_email='[email protected]',
+ description='Apache Ignite 3 DB API Driver',
+ long_description=long_description,
+ long_description_content_type='text/markdown',
+
url='https://github.com/apache/ignite-3/tree/main/modules/platforms/python',
+ packages=setuptools.find_packages(),
+ ext_modules=[CMakeExtension(EXTENSION_NAME)],
+ cmdclass=dict(build_ext=CMakeBuild),
+ install_requires=install_requirements,
+ license='Apache License 2.0',
+ license_files=('LICENSE', 'NOTICE'),
+ classifiers=[
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.7',
+ 'Programming Language :: Python :: 3.8',
+ 'Programming Language :: Python :: 3.9',
+ 'Programming Language :: Python :: 3.10',
+ 'Programming Language :: Python :: 3 :: Only',
+ 'Intended Audience :: Developers',
+ 'Topic :: Database :: Front-Ends',
+ 'Topic :: Software Development :: Libraries :: Python Modules',
+ 'License :: Free for non-commercial use',
+ 'Operating System :: OS Independent',
+ ]
+ )
+
+
+if __name__ == "__main__":
+ run_setup()
diff --git a/modules/platforms/python/tests/__init__.py
b/modules/platforms/python/tests/__init__.py
new file mode 100644
index 0000000000..3596874e86
--- /dev/null
+++ b/modules/platforms/python/tests/__init__.py
@@ -0,0 +1,14 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/modules/platforms/python/tests/conftest.py
b/modules/platforms/python/tests/conftest.py
new file mode 100644
index 0000000000..324c7d3884
--- /dev/null
+++ b/modules/platforms/python/tests/conftest.py
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import logging
+
+logger = logging.getLogger('pyignite3')
+logger.setLevel(logging.DEBUG)
diff --git a/modules/platforms/python/tests/test_connect.py
b/modules/platforms/python/tests/test_connect.py
new file mode 100644
index 0000000000..5dbd2d21d1
--- /dev/null
+++ b/modules/platforms/python/tests/test_connect.py
@@ -0,0 +1,33 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import pytest
+
+import pyignite3
+from tests.util import start_cluster_gen, check_cluster_started,
server_addresses_basic
+
+
[email protected](autouse=True)
+def cluster():
+ if not check_cluster_started():
+ yield from start_cluster_gen()
+
+
+def test_check_connection_success():
+ # TODO: Move cluster addresses in const
+ conn = pyignite3.connect(address=server_addresses_basic[0])
+ assert conn is not None
+ conn.close()
+
+
diff --git a/modules/platforms/python/tests/util.py
b/modules/platforms/python/tests/util.py
new file mode 100644
index 0000000000..fa8010d17e
--- /dev/null
+++ b/modules/platforms/python/tests/util.py
@@ -0,0 +1,155 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import contextlib
+import os
+
+import psutil
+import signal
+import subprocess
+import time
+
+import pyignite3
+
+server_addresses_basic = ['127.0.0.1:10942', '127.0.0.1:10943']
+server_addresses_ssl_basic = ['127.0.0.1:10944']
+server_addresses_ssl_client_auth = ['127.0.0.1:10945']
+server_addresses_all = server_addresses_basic + server_addresses_ssl_basic +
server_addresses_ssl_client_auth
+
+
[email protected]
+def get_or_create_cache(client, settings):
+ cache = client.get_or_create_cache(settings)
+ try:
+ yield cache
+ finally:
+ cache.destroy()
+
+
+def wait_for_condition(condition, interval=0.1, timeout=10, error=None):
+ start = time.time()
+ res = condition()
+
+ while not res and time.time() - start < timeout:
+ time.sleep(interval)
+ res = condition()
+
+ if res:
+ return True
+
+ if error is not None:
+ raise Exception(error)
+
+ return False
+
+
+def is_windows():
+ return os.name == "nt"
+
+
+def get_test_dir():
+ return os.path.dirname(os.path.realpath(__file__))
+
+
+def get_proj_dir():
+ return os.path.abspath(os.path.join(get_test_dir(), "..", "..", "..",
".."))
+
+
+def get_ignite_dirs():
+ ignite_home = os.getenv("IGNITE_HOME")
+ if ignite_home is not None:
+ yield ignite_home
+
+ yield get_proj_dir()
+
+
+def get_ignite_runner():
+ ext = ".bat" if is_windows() else ""
+ for ignite_dir in get_ignite_dirs():
+ runner = os.path.join(ignite_dir, "gradlew" + ext)
+ print("Probing Ignite runner at '{0}'...".format(runner))
+ if os.path.exists(runner):
+ return runner
+
+ raise Exception("Ignite not found. Please make sure your IGNITE_HOME
environment variable points to directory with "
+ "a valid Ignite instance")
+
+
+def kill_process_tree(pid):
+ if is_windows():
+ subprocess.call(['taskkill', '/F', '/T', '/PID', str(pid)])
+ else:
+ children = psutil.Process(pid).children(recursive=True)
+ for child in children:
+ os.kill(child.pid, signal.SIGKILL)
+ os.kill(pid, signal.SIGKILL)
+
+
+# noinspection PyBroadException
+def check_server_started(addr: str) -> bool:
+ try:
+ conn = pyignite3.connect(address=addr, timeout=1)
+ except:
+ return False
+
+ conn.close()
+ return True
+
+
+def check_cluster_started() -> bool:
+ for addr in server_addresses_basic:
+ if not check_server_started(addr):
+ return False
+ return True
+
+
+def start_cluster(debug=False, jvm_opts='') -> subprocess.Popen:
+ runner = get_ignite_runner()
+
+ env = os.environ.copy()
+
+ env['JVM_OPTS'] = env.get('JVM_OPTS', '') + jvm_opts
+
+ if debug:
+ env['JVM_OPTS'] = env.get('JVM_OPTS', '') + \
+ '-Djava.net.preferIPv4Stack=true -Xdebug -Xnoagent
-Djava.compiler=NONE ' \
+
'-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 '
+
+ ignite_cmd = [runner, ':ignite-runner:runnerPlatformTest', '--no-daemon',
'-x', 'compileJava', '-x',
+ 'compileTestFixturesJava', '-x',
'compileIntegrationTestJava', '-x', 'compileTestJava']
+
+ print('Starting Ignite runner:', ignite_cmd)
+
+ ignite_dir = next(get_ignite_dirs())
+ if ignite_dir is None:
+ raise Exception('Can not resolve an Ignite project directory')
+
+ cluster = subprocess.Popen(ignite_cmd, env=env, cwd=ignite_dir)
+
+ for addr in server_addresses_basic:
+ started = wait_for_condition(lambda: check_server_started(addr),
timeout=180)
+ if not started:
+ kill_process_tree(cluster.pid)
+ raise Exception('Failed to start Ignite Cluster: timeout while
trying to connect')
+
+ return cluster
+
+
+def start_cluster_gen(debug=False):
+ srv = start_cluster(debug=debug)
+ try:
+ yield srv
+ finally:
+ kill_process_tree(srv.pid)