#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
:Purpose: This module contains the library's *Oracle* database methods
and attribute accessors; which are a specialised version of
the :class:`_dbi_base._DBIBase` class methods.
:Platform: Linux/Windows | Python 3.10+
:Developer: J Berendt
:Email: support@s3dev.uk
:Comments: n/a
:Example:
For class-specific usage examples, please refer to the docstring
for the following classes:
- :class:`_DBIOracle`
"""
# pylint: disable=wrong-import-order
# Silence the spurious IDE-based error.
# pylint: disable=import-error
import cx_Oracle
import pandas as pd
from utils4.reporterror import reporterror
from utils4.user_interface import ui
# locals
from _dbi_base import _DBIBase
[docs]
class _DBIOracle(_DBIBase):
"""This *private* class holds the methods and properties which are
used for accessing Oracle databases.
Note:
This class is *not* designed to be interacted with directly.
Rather, please use the :class:`database.DBInterface` class
instead, as the proper interface class has an automatic switch
for database interfaces, based on the ``sqlalchemy.Engine``
object which is created from the connection string.
Args:
connstr (str): The database-specific SQLAlchemy connection
string.
:Example Use:
This low-level generalised class is designed to be inherited by
the calling/wrapping class as::
>>> from dblib.database import DBInterface
class MyDB(DBInterface):
def __init__(self, connstr: str):
super().__init__(connstr=('oracle+cx_oracle://'
'<user>:<pwd>'
'@(DESCRIPTION=(ADDRESS='
'(PROTOCOL=TCP)'
'(HOST=<host>)'
'(PORT=<port>))'
'(CONNECT_DATA='
'(SERVICE_NAME=<svcname>)))'))
"""
_ERROR_NI = ('Due to restrictions on the development environment, '
'this method is currently not implemented.')
# The __init__ method is implemented in the parent class.
[docs]
def call_procedure(self,
proc: str,
params: list | tuple = None,
return_status: bool=False) -> pd.DataFrame | tuple[pd.DataFrame | bool]:
"""Call a stored procedure, and return as a DataFrame.
Args:
proc (str): Name of the stored procedure to call.
params (list | tuple, optional): A list (or tuple) of
parameters to pass into the procedure. Defaults to None.
return_status (bool, optional): Return the method's success
status. Defaults to False.
Returns:
pd.DataFrame | tuple[pd.DataFrame | bool]:
If the ``return_status`` argument is True, a tuple of the
data and the method's return status is returned as::
(df, status)
Otherwise, only the data is returned, as a pd.DataFrame.
"""
df = pd.DataFrame()
success = False
try:
with self.engine.connect() as conn:
cur = conn.connection.cursor()
refcur = conn.connection.cursor()
cur.callproc(proc, params + [refcur])
df = self._result_to_df__refcursor(refcur=refcur)
cur.close()
refcur.close()
success = not df.empty
except cx_Oracle.DatabaseError as err:
msg = f'Error occurred while running the USP: {proc}.'
self._report_cxo_error(msg=msg, error=err)
except Exception as err:
reporterror(error=err)
return (df, success) if return_status else df
[docs]
def call_procedure_update(self):
"""Call an *update* or *insert* stored procedure.
Warning:
Due to the restrictions on the Oracle development
environment, database UPDATE-like methods are not allowed.
Therefore, this method is *currently* not implemented.
Raises:
NotImplementedError: Warn the user that this method is
currently not implemented.
"""
raise NotImplementedError(self._ERROR_NI)
[docs]
def call_procedure_update_many(self):
"""Call an *update* or *insert* stored procedure for an iterable.
Warning:
Due to the restrictions on the Oracle development
environment, database UPDATE-like methods are not allowed.
Therefore, this method is *currently* not implemented.
Raises:
NotImplementedError: Warn the user that this method is
currently not implemented.
"""
raise NotImplementedError(self._ERROR_NI)
[docs]
def call_procedure_update_raw(self):
"""Call an *update* or *insert* stored procedure, without error
handling.
Warning:
Due to the restrictions on the Oracle development
environment, database UPDATE-like methods are not allowed.
Therefore, this method is *currently* not implemented.
Raises:
NotImplementedError: Warn the user that this method is
currently not implemented.
"""
raise NotImplementedError(self._ERROR_NI)
[docs]
def table_exists(self, table_name: str, verbose: bool=False) -> bool:
"""Using the ``engine`` object, test if the given table exists.
Args:
table_name (str): Name of the table to test.
verbose (bool, optional): Print a message if the table does
not exist. Defaults to False.
Note:
As most Oracle objects are UPPERCASE, the table name is converted
to upper case before being passed into the query.
Returns:
bool: True if the given table exists, otherwise False.
"""
params = {'table_name': table_name.upper()}
stmt = 'select count(*) from all_tables where table_name = :table_name'
exists = bool(self.execute_query(stmt, params=params, raw=True)[0][0])
if (not exists) & verbose:
msg = f'Table does not exist: {table_name}, for user {self._engine.url.username}.'
ui.print_warning(text=msg)
return exists
[docs]
def _report_cxo_error(self, msg: str, error: cx_Oracle.DatabaseError):
"""Report cx_Oracle error to the terminal.
Args:
msg (str): Additional error to be displayed. This message
will be automatically prefixed with '[DatabaseError]: '
error (cx_Oracle.DatabaseError): Caught error object from the
try/except block.
"""
msg = f'{self._PREFIX} {msg}'
errr = f'- Error: {error.args[0].message}'
ui.print_alert(text=msg)
ui.print_alert(text=errr)
[docs]
@staticmethod
def _result_to_df__refcursor(refcur: cx_Oracle.Cursor) -> pd.DataFrame:
"""Convert a ``cx_Oracle.Cursor`` object to a DataFrame.
If the cursor did not return results, an empty DataFrame
containing the column names only, is returned.
Args:
refcur (cx_Oracle.Cursor): Object to be converted.
Returns:
pd.DataFrame: A ``pandas.DataFrame`` object containing the
cursor's data.
"""
return pd.DataFrame(refcur, columns=[i[0] for i in refcur.description])