"""mysql connector.""" import functools from oto import status from oto import response from sentry_sdk import capture_exception as sentry_capture_exception from sqlalchemy import create_engine from sqlalchemy import event from sqlalchemy import exc from sqlalchemy import select from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from sqlalchemy.pool import QueuePool from marketing import config from marketing.connectors.sentry import sentry_client from marketing.constants import error def _full_db_url(): db_url_parts = config.DB_URL.split('?') query = [] if len(db_url_parts) > 1: query.append(db_url_parts[1]) query.append('charset=utf8') return '{}?{}'.format(db_url_parts[0], '&'.join(query)) def autosession(capture_exception=True): """Automatically creates a session that can be used within a method. Args: capture_exception (bool): capture exceptions to sentry. Returns: callable: the encapsulated wrapper. """ return functools.partial( autosession_decorate, capture_exception=capture_exception) def autosession_decorate(function, capture_exception=True): """Decorate the function with a session context. Args: function (callable): the function to decorate. capture_exception (bool): capture exceptions to sentry. Returns: callable: decorated method. """ return functools.partial( autosession_context, function=function, capture_exception=capture_exception) def autosession_context( *args, function=None, capture_exception=True, **kwargs): """Create a session context. Args: args (tuple): tuple of arguments to pass down to the function. function (callable): the method to call after creating the session. capture_exception (bool): capture exceptions to sentry. kwargs (dict): dictionary of arguments to provide to the function. Returns: mixed: the result of the operation. In case of an exception, the error is logged into sentry and an error response is returned """ session = _db_session() try: kwargs.update(session=session) data = function(*args, **kwargs) session.commit() return data except Exception as exception: session.rollback() if capture_exception: sentry_capture_exception(exception) return response.create_error_response( code=error.ERROR_CODE_MYSQL, message=error.ERROR_MESSAGE_DB_ISSUE, status=status.INTERNAL_ERROR) finally: session.close() # please don't use the following private variables directly; # use db_session if config.POOL_CLASS == QueuePool: _db_engine = create_engine( config.DB_URL, pool_size=config.POOL_SIZE, max_overflow=config.POOL_MAX_OVERFLOW, pool_recycle=config.POOL_RECYCLE_MS, encoding='utf-8') if config.POOL_PRE_PING: @event.listens_for(_db_engine, 'engine_connect') def ping_connection(connection, branch): """Wrapper around ping connection.""" _ping_connection(connection, branch) else: _db_engine = create_engine( config.DB_URL, poolclass=config.POOL_CLASS, encoding='utf-8') # A session() instance establishes all conversations with the database # and represents a "staging zone" for all the objects loaded into the # database session object. Any change made against the objects in the # session won't be persisted into the database until you call # session.commit(). If you're not happy about the changes, you can # revert all of them back to the last commit by calling # session.rollback() # # Description taken from: # pythoncentral.io/introductory-tutorial-python-sqlalchemy/ _db_session = sessionmaker(bind=_db_engine) BaseModel = declarative_base() def _ping_connection(connection, branch): """Ping database connection after engine_connect event. This function is copied verbatim from http://docs.sqlalchemy.org/en/latest/core/pooling.html """ if branch: # "branch" refers to a sub-connection of a connection, # we don't want to bother pinging on these. return # turn off "close with result". This flag is only used with # "connectionless" execution, otherwise will be False in any case save_should_close_with_result = connection.should_close_with_result connection.should_close_with_result = False try: # run a SELECT 1. use a core select() so that # the SELECT of a scalar value without a table is # appropriately formatted for the backend connection.scalar(select([1])) except exc.DBAPIError as err: # catch SQLAlchemy's DBAPIError, which is a wrapper # for the DBAPI's exception. It includes a .connection_invalidated # attribute which specifies if this connection is a "disconnect" # condition, which is based on inspection of the original exception # by the dialect in use. if err.connection_invalidated: # run the same SELECT again - the connection will re-validate # itself and establish a new connection. The disconnect detection # here also causes the whole connection pool to be invalidated # so that all stale connections are discarded. connection.scalar(select([1])) else: raise finally: # restore "close with result" connection.should_close_with_result = save_should_close_with_result