Move exceptions into their own module
authorKarl O. Pinc <kop@karlpinc.com>
Mon, 31 Aug 2020 20:01:10 +0000 (15:01 -0500)
committerKarl O. Pinc <kop@karlpinc.com>
Tue, 1 Sep 2020 02:23:10 +0000 (21:23 -0500)
src/pgwui_core/core.py
src/pgwui_core/exceptions.py [new file with mode: 0644]

index 7cbe1245bc208ecbaf72badb91b4725beeb3ac27..79f76d6acceef2adf048f7b067470bc63619a624 100644 (file)
@@ -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 = '<br />DETAIL: ' + cgi_escape(ex.diag.message_detail)
-
-        if ex.diag.message_hint is None:
-            hint = ''
-        else:
-            hint = '<br />HINT: ' + cgi_escape(ex.diag.message_hint)
-
-        return '<blockquote><p>{0}: {1}{2}{3}</p></blockquote>'.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 (file)
index 0000000..1d92031
--- /dev/null
@@ -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
+# <http://www.gnu.org/licenses/>.
+#
+
+# Karl O. Pinc <kop@karlpinc.com>
+
+'''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 = '<br />DETAIL: ' + cgi_escape(ex.diag.message_detail)
+
+        if ex.diag.message_hint is None:
+            hint = ''
+        else:
+            hint = '<br />HINT: ' + cgi_escape(ex.diag.message_hint)
+
+        return '<blockquote><p>{0}: {1}{2}{3}</p></blockquote>'.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)