Skip to content

Commit

Permalink
Add dbapi2 compatibility shim (#145)
Browse files Browse the repository at this point in the history
---------
Co-authored-by: Christoph Kuhnke <[email protected]>
  • Loading branch information
Nicoretti authored Jul 4, 2024
1 parent 27199a6 commit 8e3b361
Show file tree
Hide file tree
Showing 19 changed files with 1,643 additions and 50 deletions.
11 changes: 6 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

## [Unreleased]

- Switch packaging and project workflows to poetry
- Drop support for python 3.7
- Drop support for Exasol 6.x
- Drop support for Exasol 7.0.x
- Relock dependencies (Internal)
- Added dbapi2 compliant driver interface `exasol.driver.websocket` ontop of pyexasol
- Switched packaging and project workflow to poetry
- Droped support for python 3.7
- Droped support for Exasol 6.x
- Droped support for Exasol 7.0.x
- Relocked dependencies (Internal)

## [0.25.2] - 2023-03-14

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ PyEXASOL provides API to read & write multiple data streams in parallel using se
## System requirements

- Exasol >= 7.1
- Python >= 3.8
- Python >= 3.9


## Getting started
Expand Down
53 changes: 22 additions & 31 deletions docs/DBAPI_COMPAT.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,28 @@
## DB-API 2.0 compatibility

PyEXASOL [public interface](/docs/REFERENCE.md) is similar to [PEP-249 DB-API 2.0](https://www.python.org/dev/peps/pep-0249/) specification, but it does not strictly follow it. This page explains the reasons behind this decision.
PyEXASOL [public interface](/docs/REFERENCE.md) is similar to [PEP-249 DB-API 2.0](https://www.python.org/dev/peps/pep-0249/) specification, but it does not strictly follow it. This page explains the reasons behind this decision and your alternative(s) if you need or want to use a dbabpi2 compatible driver..

If you absolutely need DB-API 2.0 compatibility, you may use [TurbODBC](https://github.com/blue-yonder/turbodbc) instead.
### Alternatives

#### Exasol WebSocket Driver
The `pyexasol` package includes a DBAPI2 compatible driver facade, located in the `exasol.driver` package. However, using pyexasol directly will generally yield better performance when utilizing Exasol in an OLAP manner, which is likely the typical use case.

That said, in specific scenarios, the DBAPI2 API can be advantageous or even necessary. This is particularly true when integrating with "DB-Agnostic" frameworks. In such cases, you can just import and use the DBAPI2 compliant facade as illustrated in the example below.

```python
from exasol.driver.websocket.dbapi2 import connect

connection = connect(dsn='', username='sys', password='exasol', schema='TEST')

with connection.cursor() as cursor:
cursor.execute("SELECT 1;")
```

#### TurboODBC
[TurboODBC](https://github.com/blue-yonder/turbodbc) offers an alternative ODBC-based, DBAPI2-compatible driver, which supports the Exasol database.

#### Pyodbc
[Pyodbc](https://github.com/mkleehammer/pyodbc) provides an ODBC-based, DBAPI2-compatible driver. For further details, please refer to our [wiki](https://github.com/mkleehammer/pyodbc/wiki).

### Rationale

Expand Down Expand Up @@ -66,32 +86,3 @@ Replace with:
C.export_to_pandas('SELECT * FROM table')
```
etc.

## DB-API 2.0 wrapper

In order to make it easier to start using PyEXASOL, simple DB-API 2.0 wrapper is provided. It works for `SELECT` statements without placeholders. Please see the example:

```python
# Import "wrapped" version of PyEXASOL module
import pyexasol.db2 as E

C = E.connect(dsn=config.dsn, user=config.user, password=config.password, schema=config.schema)

# Cursor
cur = C.cursor()
cur.execute("SELECT * FROM users ORDER BY user_id LIMIT 5")

# Standard .description and .rowcount attributes
print(cur.description)
print(cur.rowcount)

# Standard fetching
while True:
row = cur.fetchone()

if row is None:
break

print(row)

```
Empty file added exasol/driver/__init__.py
Empty file.
Empty file.
165 changes: 165 additions & 0 deletions exasol/driver/websocket/_connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
"""
This module provides `PEP-249`_ DBAPI compliant connection implementation.
(see also `PEP-249-connection`_)
.. _PEP-249-connection: https://peps.python.org/pep-0249/#connection-objects
"""

import ssl
from functools import wraps

import pyexasol

from exasol.driver.websocket._cursor import Cursor as DefaultCursor
from exasol.driver.websocket._errors import Error


def _requires_connection(method):
"""
Decorator requires the object to have a working connection.
Raises:
Error if the connection object has no active connection.
"""

@wraps(method)
def wrapper(self, *args, **kwargs):
if not self._connection:
raise Error("No active connection available")
return method(self, *args, **kwargs)

return wrapper


class Connection:
"""
Implementation of a websocket-based connection.
For more details see :class: `Connection` protocol definition.
"""

def __init__(
self,
dsn: str = None,
username: str = None,
password: str = None,
schema: str = "",
autocommit: bool = True,
tls: bool = True,
certificate_validation: bool = True,
client_name: str = "EXASOL:DBAPI2:WS",
client_version: str = "unknown",
):
"""
Create a Connection object.
Args:
dsn: Connection string, same format as for standard JDBC / ODBC drivers.
username: which will be used for the authentication.
password: which will be used for the authentication.
schema: to open after connecting.
autocommit: enable autocommit.
tls: enable tls.
certificate_validation: disable certificate validation.
client_name: which is communicated to the DB server.
"""

# for more details see pyexasol.connection.ExaConnection
self._options = {
"dsn": dsn,
"user": username,
"password": password,
"schema": schema,
"autocommit": autocommit,
"snapshot_transactions": None,
"connection_timeout": 10,
"socket_timeout": 30,
"query_timeout": 0,
"compression": False,
"encryption": tls,
"fetch_dict": False,
"fetch_mapper": None,
"fetch_size_bytes": 5 * 1024 * 1024,
"lower_ident": False,
"quote_ident": False,
"json_lib": "json",
"verbose_error": True,
"debug": False,
"debug_logdir": None,
"udf_output_bind_address": None,
"udf_output_connect_address": None,
"udf_output_dir": None,
"http_proxy": None,
"client_name": client_name,
"client_version": client_version,
"protocol_version": 3,
"websocket_sslopt": (
{"cert_reqs": ssl.CERT_REQUIRED} if certificate_validation else None
),
"access_token": None,
"refresh_token": None,
}
self._connection = None

def connect(self):
"""See also :py:meth: `Connection.connect`"""
try:
self._connection = pyexasol.connect(**self._options)
except pyexasol.exceptions.ExaConnectionError as ex:
raise Error(f"Connection failed, {ex}") from ex
except Exception as ex:
raise Error() from ex
return self

@property
def connection(self):
"""Underlying connection used by this Connection"""
return self._connection

def close(self):
"""See also :py:meth: `Connection.close`"""
connection_to_close = self._connection
self._connection = None
if connection_to_close is None or connection_to_close.is_closed:
return
try:
connection_to_close.close()
except Exception as ex:
raise Error() from ex

@_requires_connection
def commit(self):
"""See also :py:meth: `Connection.commit`"""
try:
self._connection.commit()
except Exception as ex:
raise Error() from ex

@_requires_connection
def rollback(self):
"""See also :py:meth: `Connection.rollback`"""
try:
self._connection.rollback()
except Exception as ex:
raise Error() from ex

@_requires_connection
def cursor(self):
"""See also :py:meth: `Connection.cursor`"""
return DefaultCursor(self)

def __del__(self):
if self._connection is None:
return

# Currently, the only way to handle this gracefully is to invoke the`__del__`
# method of the underlying connection rather than calling an explicit `close`.
#
# For more details, see also:
# * https://github.com/exasol/sqlalchemy-exasol/issues/390
# * https://github.com/exasol/pyexasol/issues/108
#
# If the above tickets are resolved, it should be safe to switch back to using
# `close` instead of `__del__`.
self._connection.__del__()
Loading

0 comments on commit 8e3b361

Please sign in to comment.