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)


Reply via email to