Skip to content
Open
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
eabb6b0
update .gitignore
mkerry-fas Mar 17, 2021
d1fffc2
draft
mkerry-fas Mar 17, 2021
2638a4b
update docs; refactor
mkerry-fas Mar 17, 2021
b01f923
rename for clarity
mkerry-fas Mar 17, 2021
a8023ac
cleanup
mkerry-fas Mar 17, 2021
7526428
rename for clarity
mkerry-fas Mar 17, 2021
df48b87
revise logger setup
mkerry-fas Mar 17, 2021
8b7da32
revise logger setup
mkerry-fas Mar 17, 2021
db21323
revise logger setup
mkerry-fas Mar 17, 2021
5c9a7c4
fix bug
mkerry-fas Mar 17, 2021
fc74b9c
add interface
mkerry-fas Mar 17, 2021
d6b2c86
refactor init params and handling
mkerry-fas Mar 18, 2021
f51d83c
cleanup
mkerry-fas Mar 18, 2021
a7e27dc
add cleanup method
mkerry-fas Mar 18, 2021
701d5b2
rename Database enum to DatabaseType; add sql alchemy
mkerry-fas Mar 19, 2021
c1d0628
update setup
mkerry-fas Mar 19, 2021
deb22af
update version
mkerry-fas Mar 19, 2021
29c3b16
rename
mkerry-fas Mar 19, 2021
549ca1a
update handling of execute_query
mkerry-fas Mar 24, 2021
eaa53f6
update documentation
mkerry-fas Mar 24, 2021
62641f9
update documentation
mkerry-fas Mar 24, 2021
f92e138
update documentation
mkerry-fas Mar 24, 2021
a261ec4
update documentation formatting
mkerry-fas Mar 24, 2021
197e803
update documentation formatting
mkerry-fas Mar 24, 2021
c0d30cc
update documentation formatting
mkerry-fas Mar 24, 2021
52a8db3
update documentation formatting
mkerry-fas Mar 24, 2021
dec7124
update documentation
mkerry-fas Mar 24, 2021
38009ea
update documentation
mkerry-fas Mar 24, 2021
e8d1027
update pylog version
mkerry-fas Mar 25, 2021
c5b9294
update pylog version url
mkerry-fas Mar 25, 2021
71eeed0
move creation of logger
mkerry-fas Mar 25, 2021
32eadeb
fix line spacing
mkerry-fas Mar 25, 2021
e3bc452
update docs
mkerry-fas Mar 25, 2021
ce67314
cleanup per PR feedback
mkerry-fas Mar 25, 2021
aa683ce
add import
mkerry-fas Mar 25, 2021
1a9ea41
update readme
mkerry-fas Mar 25, 2021
1318bb9
Update README.md
michaelkerry Mar 25, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.idea
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
68 changes: 67 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,67 @@
# pydb
# pydb

a tool for facilitating connection to databases with python and performing basic operations

## Requirements

python >= 3.7
(see setup.py for additional packages)

## Installation and setup

In a suitable python3 (>=3.7) virtual env, using pip:

pip install https://github.com/huit/pydb/archive/v0.0.2.tar.gz#egg=pydb
# import the module for the specific type of db you'd like to use
from pydb.oracle_db import OracleDB

* creating an OracleDB instance requires host, port, service, user, pwd.
* other db types may have other requirements - see specific module for details
* logging_level is optional, and will default to `logging.CRITICAL`
* logging_format is optional, and will default to pylog default formatting
* see https://github.com/huit/pylog for details

```
db = OracleDb(host="valid_host", port=8003, service="SERVICE_NAME", user="username", pwd="pwd")
# where 8003 is a valid port
```
## Basic operations

create_connection()
provides a connection to the db host for more 'direct' access

get_session()
Specific to SQL Alchemy; allows interaction with SQL Alchemy entities
see https://docs.sqlalchemy.org/en/14/orm/session.html?highlight=session#module-sqlalchemy.orm.session

execute_query(self, query_string: str, args=None) -> list
executes a sql query, return a list of dictionaries representing rows

execute_update(self, query_string, args=None)
used to execute an insert, update, or delete sql statement

health_check()
Performs a basic query against the db to ensure connectivity

cleanup()
Attempts to release any 'live' objects/connections to the host - to be run before exiting program

## Examples

Given a valid connection, and a table called `EMP`...

To query the `EMP` table for all records:

result = db.execute_query("select * from emp")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrap code examples in ticks.

I'd also like to see a super streamlined example of it put together like:

from pydb import OracleDb
from pylog import logger

db = OracleDb(host="valid_host", port=8003, service="SERVICE_NAME", user="username", pwd="pwd")
result = db.execute_query("select * from emp")
logger.info(json.dumps(result))

db.cleanup()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code wrapped; integrated example added


To query the `EMP` table for a specific record:

result = db.execute_query("select * from emp where ename= :ename", {'ename':'JOHNSON'})

Results for the individual rows would be in the following form:

{'EMPNO': 7935, 'ENAME': 'JOHNSON', 'JOB': 'CLERK', 'MGR': 7839, 'HIREDATE': datetime.datetime(1981, 5, 1, 0, 0), 'SAL': 2850.0, 'COMM': None, 'DEPTNO': 30}

Row results may vary somewhat depending on the exact module... e.g., for SqlAlchemyOracleDB the following would be received:

{'empno': 7935, 'ename': 'JOHNSON', 'job': 'CLERK', 'mgr': 7839, 'hiredate': datetime.datetime(1981, 5, 1, 0, 0), 'sal': Decimal('2850'), 'comm': None, 'deptno': 30}
Empty file added pydb/__init__.py
Empty file.
83 changes: 83 additions & 0 deletions pydb/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import abc

from enum import Enum


class DatabaseType(Enum):
"""
not implemented: other databases
"""
ORACLE = "oracle"
SQL_ALCHEMY_ORACLE = "sql_alchemy_oracle"


class DBInterface(metaclass=abc.ABCMeta):
@classmethod
def __subclasshook__(cls, subclass):
return (hasattr(subclass, 'execute_query') and
callable(subclass.execute_query) and
hasattr(subclass, 'execute_update') and
callable(subclass.execute_update) and
hasattr(subclass, 'health_check') and
callable(subclass.health_check) and
hasattr(subclass, 'cleanup') and
callable(subclass.cleanup) and
hasattr(subclass, 'create_connection') and
callable(subclass.create_connection) and
hasattr(subclass, 'get_session') and
callable(subclass.get_session)
or NotImplemented)
Copy link
Copy Markdown
Collaborator

@ccurreri ccurreri Mar 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a specific reason you're overriding this?

You already are handling this with the abstractmethod decorators. If a subclass fails to implement one of these it can't be instantiated.

>>> from abc import ABC, abstractmethod
>>>
>>> class Foo(ABC):
...     @abstractmethod
...     def bar(self):
...             pass
...
>>> class Baz(Foo):
...     pass
...
>>> x = Baz()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Baz with abstract method bar

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated - removed


@abc.abstractmethod
def execute_query(self, query_string: str, args=None) -> list:
"""
executes a sql query, return a list of dictionaries representing rows
:param self:
:param query_string:
:param args:
:return:
"""
raise NotImplementedError

@abc.abstractmethod
def execute_update(self, query_string, args=None):
"""
used to execute an insert, update, or delete sql statement
:param self:
:param query_string:
:param args:
:return:
"""
raise NotImplementedError

@abc.abstractmethod
def health_check():
"""
Performs a basic query against the db to ensure connectivity
:return:
"""
raise NotImplementedError

@abc.abstractmethod
def cleanup():
"""
Attempts to release any 'live' objects/connections to the host
:return:
"""
raise NotImplementedError

@abc.abstractmethod
def create_connection():
"""
provides a connection to the db host for more 'direct' access
:return:
"""
raise NotImplementedError

@abc.abstractmethod
def get_session():
"""
Specific to SQL Alchemy; allows interaction with SQL Alchemy entities
:return:
"""
raise NotImplementedError
182 changes: 182 additions & 0 deletions pydb/oracle_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
#!/usr/bin/env python3
"""
Module for interacting with an oracle database
"""
# -*- encoding: utf-8 -*-

#============================================================================================
# Imports
#============================================================================================
# Standard imports
import logging

# Third-party imports
import cx_Oracle

from pylog.pylog import get_common_logger_for_module

from .database import DBInterface

# Local imports


class OracleDB(DBInterface):
"""
Class for interacting with an oracle database
"""

def __init__(self, host: str, port: int, service: str, user: str, pwd: str,
logging_level: int = 50, logging_format: logging.Formatter = None):
"""
Setup for oracle db connections. oracle_config must be a python dictionary with the following fields:

:param host:
:param port:
:param service:
:param user:
:param pwd:
:param logging_level:
:param logging_format:
:param logging_level: defaults to logging.CRITICAL
:param logging_format: defaults to None here, which translates to the pylog.get_commong_logging_format
"""

self.host = host
self.port = port
self.service = service
self.user = user
self.pwd = pwd
self.pool = self.set_up_session_pool()

self.logger = get_common_logger_for_module(module_name=__name__, level=logging_level, log_format=logging_format)

def set_up_session_pool(self):
try:
dsn_str = cx_Oracle.makedsn(self.host, self.port, service_name=self.service)
pool = cx_Oracle.SessionPool(
user=self.user,
password=self.pwd,
dsn=dsn_str,
min=2,
max=5,
increment=1,
threaded=True,
encoding="UTF-8"
)
self.pool = pool
return pool

except cx_Oracle.DatabaseError as err:
obj, = err.args
self.logger.error("Error creating pool")
self.logger.error("Context: %s", obj.context)
self.logger.error("Message: %s", obj.message)
raise Exception(f"Error creating pool: {obj.message}")

def get_session_pool(self):
"""
Function for creating a session pool with the database
"""
if self.pool:
return self.pool
else:
self.set_up_session_pool()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The else clause here technically will never fire because you always set self.pool in __init__ or throw an error.

One alternative would be to just use a property for this to load it on demand:

class OracleDB(DBInterface):
    def __init__(self, host: str, port: int, service: str, user: str, pwd: str,
                       logging_level: int = 50, logging_format: logging.Formatter = None):

        . . .

        self._pool = None

        . . .
        
    @property
    def pool(self):
        if not self._pool:
           self._pool = self.set_up_session_pool()

        return self._pool

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated


def create_connection(self):
"""
Function for creating a connection with the database from a session pool
"""
try:
connection = self.get_session_pool().acquire()
return connection
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could just be:

return self.get_session_pool().acquire()

or with the changes above just:

return self.pool.acquire()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated


except cx_Oracle.DatabaseError as err:
obj, = err.args
self.logger.error("Error acquiring database connection from the session pool")
self.logger.error("Context: %s", obj.context)
self.logger.error("Message: %s", obj.message)
raise Exception("Error acquiring database connection from the session pool")

@staticmethod
def make_dict(cursor):
"""
Function for converting a query result row into a dictionary
"""
column_names = [d[0] for d in cursor.description]

def create_row(*args):
return dict(zip(column_names, args))
return create_row
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean return create_row() here or do you actually mean to return the function?

If the latter this is something a lambda could do relatively easily:

return lambda *args: dict(zip(column_names, args))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I 'inherited' this syntax - and am opting to leave as is since it is working as expected


def execute_query(self, query_string: str, args=None) -> dict:
"""
Function for executing a query against the database via the session pool
"""
try:
connection = self.create_connection()
cursor = connection.cursor()
if args is not None:
cursor.execute(query_string, args)
else:
cursor.execute(query_string)
cursor.rowfactory = self.make_dict(cursor)
query_result = cursor.fetchall()
return query_result

except cx_Oracle.DatabaseError as err:
obj, = err.args
self.logger.error("Error in execute_query")
self.logger.error("Context: %s", obj.context)
self.logger.error("Message: %s", obj.message)
raise Exception(f"Error executing query: {query_string}")

finally:
cursor.close()
self.get_session_pool().release(connection)

def execute_update(self, query_string, args=None):
"""
Function for executing an insert/update query against the database via the session pool
"""
try:
connection = self.create_connection()
cursor = connection.cursor()
if args is not None:
cursor.execute(query_string, args)
else:
cursor.execute(query_string)
connection.commit()

except cx_Oracle.DatabaseError as err:
obj, = err.args
self.logger.error("Error in execute_update")
self.logger.error("Context: %s", obj.context)
self.logger.error("Message: %s", obj.message)
raise Exception(f"Error executing update: {query_string}")

finally:
cursor.close()
self.get_session_pool().release(connection)

def health_check(self):
"""
provides a means to verify DB connectivity with a simple query
:return:
"""
return self.execute_query("SELECT 1 FROM DUAL")

def cleanup(self):
if self.pool is not None:
self.logger.info("Active session pool found. Attempting to close session pool.")
try:
self.pool.close(force=True)
self.logger.info("Session pool successfully closed.")

except cx_Oracle.Error as err:
self.logger.error("Unable to close the active session.", exc_info=True)
obj, = err.args
self.logger.error("Context:", obj.context)
self.logger.error("Message:", obj.message)

def get_session(self):
return None
Loading