-
Notifications
You must be signed in to change notification settings - Fork 0
draft #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
draft #1
Changes from 28 commits
eabb6b0
d1fffc2
2638a4b
b01f923
a8023ac
7526428
df48b87
8b7da32
db21323
5c9a7c4
fc74b9c
d6b2c86
f51d83c
a7e27dc
701d5b2
c1d0628
deb22af
29c3b16
549ca1a
eaa53f6
62641f9
f92e138
a261ec4
197e803
c0d30cc
52a8db3
dec7124
38009ea
e8d1027
c5b9294
71eeed0
32eadeb
e3bc452
ce67314
aa683ce
1a9ea41
1318bb9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| .idea | ||
| # Byte-compiled / optimized / DLL files | ||
| __pycache__/ | ||
| *.py[cod] | ||
|
|
||
| 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") | ||
|
|
||
| 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} | ||
| 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) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| 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() | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 One alternative would be to just use a property for this to load it on demand:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could just be: or with the changes above just:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mean If the latter this is something a lambda could do relatively easily:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment.
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:
There was a problem hiding this comment.
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