From 31524db10c5a0d4545c898f09b1ef19df7abebed Mon Sep 17 00:00:00 2001 From: "Karl O. Pinc" Date: Mon, 31 Aug 2020 15:01:10 -0500 Subject: [PATCH] Move exceptions into their own module --- src/pgwui_core/core.py | 288 +++++++---------------------------- src/pgwui_core/exceptions.py | 217 ++++++++++++++++++++++++++ 2 files changed, 269 insertions(+), 236 deletions(-) create mode 100644 src/pgwui_core/exceptions.py diff --git a/src/pgwui_core/core.py b/src/pgwui_core/core.py index 7cbe124..79f76d6 100644 --- a/src/pgwui_core/core.py +++ b/src/pgwui_core/core.py @@ -39,13 +39,15 @@ from __future__ import absolute_import from __future__ import division from csv import reader as csv_reader -from cgi import escape as cgi_escape import collections.abc import ast import markupsafe import hashlib + import io +from . import exceptions as core_ex + # We are not really using wtforms. We use it to (barely) # interact with the html and post request but really # we define our own classes to handle working memory @@ -612,7 +614,7 @@ def format_exception(ex): msg += ', detail={0}'.format(escape_eol(diag.message_detail)) if hasattr(diag, 'message_hint'): msg += ', hint={0}'.format(escape_eol(diag.message_hint)) - elif isinstance(ex, UploadError): + elif isinstance(ex, core_ex.UploadError): msg = ex.e if ex.descr != '': msg += ' {0}'.format(escape_eol(ex.descr)) @@ -625,195 +627,6 @@ def format_exception(ex): return msg -# Error handling - -class UploadError(Exception): - ''' - Module exceptions are derived from this class. - - lineno Line number to which error pertains, if any - e The error message - descr More description of the error - detail Extra HTML describing the error - data Line of data causing problem, if any - - UploadError - * Error - * NoHeadersError - * NoDataError - * DBError - * DBCommitError - * DBDataLineError - * DataLineError - * TooManyColsError - ''' - def __init__(self, e, lineno='', descr='', detail='', data=''): - super(UploadError, self).__init__() - self.lineno = lineno - self.e = e - self.descr = descr - self.detail = detail - self.data = data - - def __str__(self): - out = 'error ({0})'.format(self.e) - if self.lineno != '': - out = '{0}: lineno ({1})'.format(out, self.lineno) - if self.descr != '': - out = '{0}: descr ({1})'.format(out, self.descr) - if self.detail != '': - out = '{0}: detail ({1})'.format(out, self.detail) - if self.data != '': - out = '{0}: data ({1})'.format(out, self.data) - return out - - -class Error(UploadError): - ''' - Module exceptions rasied while setting up to read data lines - are derived from this class. - - e The error message - descr More description of the error - detail Extra HTML describing the error - ''' - def __init__(self, e, descr='', detail=''): - super(Error, self).__init__(e=e, descr=descr, detail=detail) - - -class NoFileError(Error): - '''No file uploaded''' - def __init__(self, e, descr='', detail=''): - super(NoFileError, self).__init__(e, descr, detail) - - -class NoDBError(Error): - '''No database name given''' - def __init__(self, e, descr='', detail=''): - super(NoDBError, self).__init__(e, descr, detail) - - -class NoUserError(Error): - '''No user name supplied''' - def __init__(self, e, descr='', detail=''): - super(NoUserError, self).__init__(e, descr, detail) - - -class AuthFailError(Error): - '''Unable to connect to the db''' - def __init__(self, e, descr='', detail=''): - super(AuthFailError, self).__init__(e, descr, detail) - - -class DryRunError(Error): - '''Rollback due to dry_run config option''' - def __init__(self, e, descr='', detail=''): - super(DryRunError, self).__init__(e, descr, detail) - - -class CSRFError(Error): - '''Invalid CSRF token''' - def __init__(self, e, descr='', detail=''): - super(CSRFError, self).__init__(e, descr, detail) - - -class NoHeadersError(Error): - '''No column headings found''' - def __init__(self, e, descr='', detail=''): - super(NoHeadersError, self).__init__(e, descr, detail) - - -class NoDataError(Error): - '''No data uploaded''' - def __init__(self, e, descr='', detail=''): - super(NoDataError, self).__init__(e, descr, detail) - - -class DuplicateUploadError(Error): - '''The same filename updated twice into the same db''' - def __init__(self, e, descr='', detail=''): - super(DuplicateUploadError, self).__init__(e, descr, detail) - - -class DataInconsistencyError(Error): - def __init__(self, e, descr='', detail=''): - super(DataInconsistencyError, self).__init__(e, descr, detail) - - -class DBError(Error): - '''psycopg2 raised an error''' - def __init__(self, pgexc, e='process your request'): - ''' - pgexc The psycopg2 exception object - e Description of what PG was doing - ''' - super(DBError, self).__init__( - 'PostgreSQL is unable to ' + e + ':', - 'It reports:', - self.html_blockquote(pgexc)) - - def html_blockquote(self, ex): - ''' - Produce an html formatted message from a psycopg2 DatabaseError - exception. - ''' - primary = cgi_escape(ex.diag.message_primary) - - if ex.diag.message_detail is None: - detail = '' - else: - detail = '
DETAIL: ' + cgi_escape(ex.diag.message_detail) - - if ex.diag.message_hint is None: - hint = '' - else: - hint = '
HINT: ' + cgi_escape(ex.diag.message_hint) - - return '

{0}: {1}{2}{3}

'.format( - ex.diag.severity, - primary, - detail, - hint) - - -class DBCommitError(DBError): - def __init__(self, pgexc): - super(DBCommitError, self).__init__(pgexc) - - -class DBDataLineError(DBError): - '''Database generated an error while the processor was running.''' - - def __init__(self, udl, pgexc): - ''' - udl An UploadDataLine instance - pgexc The psycopg2 exception object - ''' - super(DBDataLineError, self).__init__(pgexc) - self.lineno = udl.lineno - self.data = udl.raw - - -class DataLineError(UploadError): - ''' - Module exceptions rasied while line-by-line processing the uploaded - data are derived from this class. - - lineno The line number - e The error message - descr More description of the error - detail Extra HTML describing the error - data The uploaded data - ''' - def __init__(self, lineno, e, descr='', detail='', data=''): - super(DataLineError, self).__init__(e, lineno, descr, detail, data) - - -class TooManyColsError(DataLineError): - def __init__(self, lineno, e, descr='', detail='', data=''): - super(TooManyColsError, self).__init__(lineno, e, descr, detail, data) - - # Upload processing class SQLCommand(object): @@ -885,7 +698,7 @@ class LogSQLCommand(SQLCommand): ''' try: super(LogSQLCommand, self).execute(cur) - except (UploadError, psycopg2.DatabaseError) as ex: + except (core_ex.UploadError, psycopg2.DatabaseError) as ex: if self.log_failure: self.log_failure(ex) raise @@ -946,8 +759,9 @@ class UploadHeaders(UploadLine): def __init__(self, line, stol, mapper): if mapper(line) == '': - raise NoHeadersError('No column headings found on first line', - 'The first line is ({0})'.format(line)) + raise core_ex.NoHeadersError( + 'No column headings found on first line', + 'The first line is ({0})'.format(line)) super(UploadHeaders, self).__init__(line, stol, mapper) self.sql = ', '.join(['"' + doublequote(st) + '"' @@ -1045,7 +859,7 @@ class UploadData(DBData): try: line = next(self._fileo) except StopIteration: - raise NoDataError('Uploaded file contains no data') + raise core_ex.NoDataError('Uploaded file contains no data') else: self.lineno += 1 # Intuit the eol sequence @@ -1131,10 +945,10 @@ class UploadData(DBData): If there's too many elements, raise an error. ''' if len(seq) > self.cols: - raise TooManyColsError(self.lineno, - 'Line has too many columns', - 'More columns than column headings', - data=line) + raise core_ex.TooManyColsError(self.lineno, + 'Line has too many columns', + 'More columns than column headings', + data=line) return seq + ['' for i in range(len(seq) + 1, self.cols)] @@ -1169,7 +983,7 @@ class DataLineProcessor(object): udl An UploadDataLine instance ''' - raise NotImplementedError + raise core_ex.NotImplementedError class NoOpProcessor(DataLineProcessor): @@ -1250,13 +1064,13 @@ class DBHandler(object): Return an instantiation of the upload form needed by the upload handler. ''' - raise NotImplementedError + raise core_ex.NotImplementedError def get_data(self): ''' Put something that will go into the db into the 'data' attribute. ''' - raise NotImplementedError + raise core_ex.NotImplementedError def val_input(self): ''' @@ -1282,7 +1096,7 @@ class DBHandler(object): Returns: Dict pyramid will use to render the resulting form Reserved keys: - errors A list of UploadError exceptions. + errors A list of core_ex.UploadError exceptions. ''' return self.uf.write(result, errors) @@ -1334,7 +1148,7 @@ class SessionDBHandler(DBHandler): Returns: Dict pyramid will use to render the resulting form Reserved keys: - errors A list of UploadError exceptions. + errors A list of core_ex.UploadError exceptions. csrf_token Token for detecting CSRF. ''' response = super(SessionDBHandler, self).write(result, errors) @@ -1375,7 +1189,7 @@ class UploadHandler(SessionDBHandler): errors = super(UploadHandler, self).val_input() if uf['filename'] == '': - errors.append(NoFileError('No file supplied')) + errors.append(core_ex.NoFileError('No file supplied')) return errors @@ -1390,7 +1204,7 @@ class UploadHandler(SessionDBHandler): ''' uf = self.uf if self.make_double_key() == uf['last_key']: - errors.append(DuplicateUploadError( + errors.append(core_ex.DuplicateUploadError( 'File just uploaded to this db', ('File named ({0}) just uploaded' .format(markupsafe.escape(uf['filename']))), @@ -1443,7 +1257,7 @@ class UploadHandler(SessionDBHandler): Returns: Dict pyramid will use to render the resulting form Reserved keys: - errors A list of UploadError exceptions. + errors A list of core_ex.UploadError exceptions. csrf_token Token for detecting CSRF. e_cnt Number of errors. db_changed Boolean. Whether the db was changed. @@ -1476,7 +1290,7 @@ class TabularFileUploadHandler(UploadHandler): '''Finish after processing all lines.''' lines = self.ue.data.lineno if lines == 1: - raise DataLineError( + raise core_ex.DataLineError( 1, 'File contains no data', ('No lines found after ' @@ -1547,18 +1361,20 @@ class DBConnector(object): return {'havecreds': False} def nodberror_factory(self): - return NoDBError('No database name supplied') + return core_ex.NoDBError('No database name supplied') def nousererror_factory(self): - return NoUserError('No user name supplied as login credentials') + return core_ex.NoUserError( + 'No user name supplied as login credentials') def authfailerror_factory(self): - return AuthFailError('Unable to login', - 'Is the database, user, and password correct?') + return core_ex.AuthFailError( + 'Unable to login', + 'Is the database, user, and password correct?') def dryrunerror_factory(self): - return DryRunError('Configured for "dry_run":' - ' Transaction deliberately rolled back') + return core_ex.DryRunError('Configured for "dry_run":' + ' Transaction deliberately rolled back') def upload_data(self, data, errors): '''Put a DBData object into the db. @@ -1595,7 +1411,7 @@ class DBConnector(object): # (Cannot call uh until after self is fully # initalized, including self.cur.) processor = self.uh.factory(self) - except Error as ex: + except core_ex.Error as ex: errors.append(ex) else: try: @@ -1603,7 +1419,7 @@ class DBConnector(object): # Let upload handler finish try: self.uh.cleanup() - except UploadError as ex: + except core_ex.UploadError as ex: errors.append(ex) finally: self.cur.close() @@ -1615,11 +1431,11 @@ class DBConnector(object): the connection. func(conn) Call this function with the connection. - func(conn) must return a list of Error instances + func(conn) must return a list of core_ex.Error instances Returns: (errors, response) - errors List of Error instances + errors List of core_ex.Error instances response Dict pyramid will use to render the resulting form. The dict returned by func(conn) plus reserved keys. Reserved keys: @@ -1705,7 +1521,7 @@ class DBConnector(object): Returns: (errors, response) - errors List of Error instantiations + errors List of core_ex.Error instantiations response Dict containing connection result info Side effects: @@ -1756,10 +1572,10 @@ class NoTransactionEngine(DBConnector): a transaction. func(conn) Call this function with the connection. - func(conn) must return a list of Error instances + func(conn) must return a list of core_ex.Error instances Returns: - errors List of Error instances + errors List of core_ex.Error instances Side effects: Calls func(conn) ''' @@ -1783,16 +1599,16 @@ class NoTransactionEngine(DBConnector): for thunk in data: try: udl = thunk() - except DataLineError as ex: + except core_ex.DataLineError as ex: errors.append(ex) else: try: processor.eat(udl) except psycopg2.DatabaseError as ex: - errors.append(DBDataLineError(udl, ex)) - except DataLineError as ex: + errors.append(core_ex.DBDataLineError(udl, ex)) + except core_ex.DataLineError as ex: errors.append(ex) - except DBError as ex: + except core_ex.DBError as ex: errors.append(ex) @@ -1829,10 +1645,10 @@ class UnsafeUploadEngine(DBConnector): Call a database modification function with a connection. func(conn) Call this function with the connection. - func(conn) must return a list of Error instances + func(conn) must return a list of core_ex.Error instances Returns: - errors List of Error instances + errors List of core_ex.Error instances Side effects: Calls func(conn) ''' @@ -1849,7 +1665,7 @@ class UnsafeUploadEngine(DBConnector): try: conn.commit() except psycopg2.DatabaseError as ex: - errors.append(DBCommitError(ex)) + errors.append(core_ex.DBCommitError(ex)) conn.close() return errors @@ -1864,7 +1680,7 @@ class UnsafeUploadEngine(DBConnector): try: result = thunk() except psycopg2.DatabaseError as ex: - raise DBDataLineError(udl, ex) + raise core_ex.DBDataLineError(udl, ex) else: return result @@ -1883,7 +1699,7 @@ class UnsafeUploadEngine(DBConnector): for thunk in data: try: udl = thunk() - except DataLineError as ex: + except core_ex.DataLineError as ex: errors.append(ex) else: self.cur.execute( @@ -1893,12 +1709,12 @@ class UnsafeUploadEngine(DBConnector): except psycopg2.DatabaseError as ex: self.cur.execute( 'ROLLBACK TO line_savepoint;') - errors.append(DBDataLineError(udl, ex)) - except DataLineError as ex: + errors.append(core_ex.DBDataLineError(udl, ex)) + except core_ex.DataLineError as ex: self.cur.execute( 'ROLLBACK TO line_savepoint;') errors.append(ex) - except DBError as ex: + except core_ex.DBError as ex: self.cur.execute( 'ROLLBACK TO line_savepoint;') errors.append(ex) @@ -1934,7 +1750,7 @@ class UploadEngine(UnsafeUploadEngine): super(UploadEngine, self).__init__(uh) def csrferror_factory(self): - return CSRFError( + return core_ex.CSRFError( 'Your request failed and you are now logged out', ('This is a security measure. ' 'Some possible causes are:'), @@ -1970,12 +1786,12 @@ class UploadEngine(UnsafeUploadEngine): func(conn) Call this function with the connection. f(conn) must return a (errors, dict) tuple result, - errors list of Error instances + errors list of core_ex.Error instances dict other results Returns: (errors, response) - errors List of Error instances + errors List of core_ex.Error instances response Dict pyramid will use to render the resulting form. The dict returned by func(conn) plus reserved keys. Reserved keys: diff --git a/src/pgwui_core/exceptions.py b/src/pgwui_core/exceptions.py new file mode 100644 index 0000000..1d92031 --- /dev/null +++ b/src/pgwui_core/exceptions.py @@ -0,0 +1,217 @@ +# Copyright (C) 2013, 2014, 2015, 2018, 2020 The Meme Factory, Inc. +# http://www.karlpinc.com/ + +# This file is part of PGWUI_Core. +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation, either version 3 of +# the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# + +# Karl O. Pinc + +'''Exceptions +''' + +from cgi import escape as cgi_escape + + +class PGWUIError(Exception): + pass + + +class UploadError(PGWUIError): + ''' + Module exceptions are derived from this class. + + lineno Line number to which error pertains, if any + e The error message + descr More description of the error + detail Extra HTML describing the error + data Line of data causing problem, if any + + UploadError + * Error + * NoHeadersError + * NoDataError + * DBError + * DBCommitError + * DBDataLineError + * DataLineError + * TooManyColsError + ''' + def __init__(self, e, lineno='', descr='', detail='', data=''): + super(UploadError, self).__init__() + self.lineno = lineno + self.e = e + self.descr = descr + self.detail = detail + self.data = data + + def __str__(self): + out = 'error ({0})'.format(self.e) + if self.lineno != '': + out = '{0}: lineno ({1})'.format(out, self.lineno) + if self.descr != '': + out = '{0}: descr ({1})'.format(out, self.descr) + if self.detail != '': + out = '{0}: detail ({1})'.format(out, self.detail) + if self.data != '': + out = '{0}: data ({1})'.format(out, self.data) + return out + + +class Error(UploadError): + ''' + Module exceptions rasied while setting up to read data lines + are derived from this class. + + e The error message + descr More description of the error + detail Extra HTML describing the error + ''' + def __init__(self, e, descr='', detail=''): + super(Error, self).__init__(e=e, descr=descr, detail=detail) + + +class NoFileError(Error): + '''No file uploaded''' + def __init__(self, e, descr='', detail=''): + super(NoFileError, self).__init__(e, descr, detail) + + +class NoDBError(Error): + '''No database name given''' + def __init__(self, e, descr='', detail=''): + super(NoDBError, self).__init__(e, descr, detail) + + +class NoUserError(Error): + '''No user name supplied''' + def __init__(self, e, descr='', detail=''): + super(NoUserError, self).__init__(e, descr, detail) + + +class AuthFailError(Error): + '''Unable to connect to the db''' + def __init__(self, e, descr='', detail=''): + super(AuthFailError, self).__init__(e, descr, detail) + + +class DryRunError(Error): + '''Rollback due to dry_run config option''' + def __init__(self, e, descr='', detail=''): + super(DryRunError, self).__init__(e, descr, detail) + + +class CSRFError(Error): + '''Invalid CSRF token''' + def __init__(self, e, descr='', detail=''): + super(CSRFError, self).__init__(e, descr, detail) + + +class NoHeadersError(Error): + '''No column headings found''' + def __init__(self, e, descr='', detail=''): + super(NoHeadersError, self).__init__(e, descr, detail) + + +class NoDataError(Error): + '''No data uploaded''' + def __init__(self, e, descr='', detail=''): + super(NoDataError, self).__init__(e, descr, detail) + + +class DuplicateUploadError(Error): + '''The same filename updated twice into the same db''' + def __init__(self, e, descr='', detail=''): + super(DuplicateUploadError, self).__init__(e, descr, detail) + + +class DataInconsistencyError(Error): + def __init__(self, e, descr='', detail=''): + super(DataInconsistencyError, self).__init__(e, descr, detail) + + +class DBError(Error): + '''psycopg2 raised an error''' + def __init__(self, pgexc, e='process your request'): + ''' + pgexc The psycopg2 exception object + e Description of what PG was doing + ''' + super(DBError, self).__init__( + 'PostgreSQL is unable to ' + e + ':', + 'It reports:', + self.html_blockquote(pgexc)) + + def html_blockquote(self, ex): + ''' + Produce an html formatted message from a psycopg2 DatabaseError + exception. + ''' + primary = cgi_escape(ex.diag.message_primary) + + if ex.diag.message_detail is None: + detail = '' + else: + detail = '
DETAIL: ' + cgi_escape(ex.diag.message_detail) + + if ex.diag.message_hint is None: + hint = '' + else: + hint = '
HINT: ' + cgi_escape(ex.diag.message_hint) + + return '

{0}: {1}{2}{3}

'.format( + ex.diag.severity, + primary, + detail, + hint) + + +class DBCommitError(DBError): + def __init__(self, pgexc): + super(DBCommitError, self).__init__(pgexc) + + +class DBDataLineError(DBError): + '''Database generated an error while the processor was running.''' + + def __init__(self, udl, pgexc): + ''' + udl An UploadDataLine instance + pgexc The psycopg2 exception object + ''' + super(DBDataLineError, self).__init__(pgexc) + self.lineno = udl.lineno + self.data = udl.raw + + +class DataLineError(UploadError): + ''' + Module exceptions rasied while line-by-line processing the uploaded + data are derived from this class. + + lineno The line number + e The error message + descr More description of the error + detail Extra HTML describing the error + data The uploaded data + ''' + def __init__(self, lineno, e, descr='', detail='', data=''): + super(DataLineError, self).__init__(e, lineno, descr, detail, data) + + +class TooManyColsError(DataLineError): + def __init__(self, lineno, e, descr='', detail='', data=''): + super(TooManyColsError, self).__init__(lineno, e, descr, detail, data) -- 2.34.1