From d6d0d83a94336d078c96af562b9436aed9cb792f Mon Sep 17 00:00:00 2001 From: Michal Charemza Date: Fri, 15 Mar 2024 18:17:14 +0000 Subject: [PATCH] feat: slightly more-specific exceptions raised To help exception handling just that little bit more --- README.md | 19 +++++++++++++++++++ sqlite_s3_query.py | 26 +++++++++++++++++++++----- test.py | 26 ++++++++++++++++---------- 3 files changed, 56 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 5130aa8..3a357bb 100644 --- a/README.md +++ b/README.md @@ -238,6 +238,25 @@ This means that sqlite-s3-query is not for all use cases of querying SQLite data This is not necessarily a permanent decision - it is possible that in future sqlite-s3-query will support unversioned buckets. +## Exception hierarchy + +- `SQLiteS3QueryError` + + The base class for explicitly raised exceptions. + + - `VersioningNotEnabledError` + + Versioning is not enabled on the bucket. + + - `QueryContextClosedError` + + A results iterable has been attempted to be used after the close of its surrounding query context. + + - `SQLiteError` + + SQLite has detected an error. The first element of the `args` member of the raised exception is the description of the error as provided by SQLite. + + ## Compatibility - Linux (tested on Ubuntu 20.04), Windows (tested on Windows Server 2019), or macOS (tested on macOS 11) diff --git a/sqlite_s3_query.py b/sqlite_s3_query.py index ce0f703..2c5e64e 100644 --- a/sqlite_s3_query.py +++ b/sqlite_s3_query.py @@ -72,11 +72,11 @@ def sqlite_s3_query_multi(url, get_credentials=lambda now: ( def run(func, *args): res = func(*args) if res != 0: - raise Exception(libsqlite3.sqlite3_errstr(res).decode()) + raise SQLiteError(libsqlite3.sqlite3_errstr(res).decode()) def run_with_db(db, func, *args): if func(*args) != 0: - raise Exception(libsqlite3.sqlite3_errmsg(db).decode()) + raise SQLiteError(libsqlite3.sqlite3_errmsg(db).decode()) @contextmanager def make_auth_request(http_client, method, params, headers): @@ -148,7 +148,7 @@ def get_vfs(http_client): try: version_id = head_headers['x-amz-version-id'] except KeyError: - raise Exception('The bucket must have versioning enabled') + raise VersioningNotEnabledError('The bucket must have versioning enabled') size = int(head_headers['content-length']) @@ -292,7 +292,7 @@ def get_pp_stmt(statement): try: return statements[statement] except KeyError: - raise Exception('Attempting to use finalized statement') from None + raise QueryContextClosedError('Attempting to use finalized statement') from None def finalize(statement): pp_stmt = statements.pop(statement) @@ -328,7 +328,7 @@ def rows(get_pp_stmt, columns): if res == SQLITE_DONE: break if res != SQLITE_ROW: - raise Exception(libsqlite3.sqlite3_errstr(res).decode()) + raise SQLiteError(libsqlite3.sqlite3_errstr(res).decode()) yield tuple( extract[libsqlite3.sqlite3_column_type(pp_stmt, i)](pp_stmt, i) @@ -394,3 +394,19 @@ def query(query_base, sql, params=(), named_params=()): ) as query_base: yield partial(query, query_base) + + +class SQLiteS3QueryError(Exception): + pass + + +class VersioningNotEnabledError(SQLiteS3QueryError): + pass + + +class SQLiteError(SQLiteS3QueryError): + pass + + +class QueryContextClosedError(SQLiteS3QueryError): + pass diff --git a/test.py b/test.py index 5634a65..c102647 100644 --- a/test.py +++ b/test.py @@ -16,7 +16,13 @@ import httpx -from sqlite_s3_query import sqlite_s3_query, sqlite_s3_query_multi +from sqlite_s3_query import ( + VersioningNotEnabledError + SQLiteError, + QueryContextClosedError, + sqlite_s3_query, + sqlite_s3_query_multi, +) class TestSqliteS3Query(unittest.TestCase): @@ -36,7 +42,7 @@ def test_without_versioning(self): ]) as db: put_object_without_versioning('bucket-without-versioning', 'my.db', db) - with self.assertRaisesRegex(Exception, 'The bucket must have versioning enabled'): + with self.assertRaisesRegex(VersioningNotEnabledError, 'The bucket must have versioning enabled'): sqlite_s3_query('http://localhost:9000/bucket-without-versioning/my.db', get_credentials=lambda now: ( 'us-east-1', 'AKIAIOSFODNN7EXAMPLE', @@ -98,7 +104,7 @@ def test_select(self): self.assertEqual(rows, [('some-text-a',)] * 500) - with self.assertRaisesRegex(Exception, 'Attempting to use finalized statement'): + with self.assertRaisesRegex(QueryContextClosedError, 'Attempting to use finalized statement'): with sqlite_s3_query('http://localhost:9000/my-bucket/my.db', get_credentials=lambda now: ( 'us-east-1', 'AKIAIOSFODNN7EXAMPLE', @@ -110,7 +116,7 @@ def test_select(self): break next(rows) - with self.assertRaisesRegex(Exception, 'Attempting to use finalized statement'): + with self.assertRaisesRegex(QueryContextClosedError, 'Attempting to use finalized statement'): with sqlite_s3_query('http://localhost:9000/my-bucket/my.db', get_credentials=lambda now: ( 'us-east-1', 'AKIAIOSFODNN7EXAMPLE', @@ -257,7 +263,7 @@ def test_select_multi(self): next(rows_2_it) raise Exception('Multiple open statements') - with self.assertRaisesRegex(Exception, 'Attempting to use finalized statement'): + with self.assertRaisesRegex(QueryContextClosedError, 'Attempting to use finalized statement'): with sqlite_s3_query_multi('http://localhost:9000/my-bucket/my.db', get_credentials=lambda now: ( 'us-east-1', 'AKIAIOSFODNN7EXAMPLE', @@ -393,7 +399,7 @@ def test_non_existant_table(self): 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', None, ), get_libsqlite3=get_libsqlite3) as query: - with self.assertRaisesRegex(Exception, 'no such table: non_table'): + with self.assertRaisesRegex(SQLiteError, 'no such table: non_table'): query("SELECT * FROM non_table").__enter__() def test_empty_object(self): @@ -405,7 +411,7 @@ def test_empty_object(self): 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', None, ), get_libsqlite3=get_libsqlite3) as query: - with self.assertRaisesRegex(Exception, 'disk I/O error'): + with self.assertRaisesRegex(SQLiteError, 'disk I/O error'): query('SELECT 1').__enter__() def test_bad_db_header(self): @@ -417,7 +423,7 @@ def test_bad_db_header(self): 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', None, ), get_libsqlite3=get_libsqlite3) as query: - with self.assertRaisesRegex(Exception, 'disk I/O error'): + with self.assertRaisesRegex(SQLiteError, 'disk I/O error'): query("SELECT * FROM non_table").__enter__() def test_bad_db_second_half(self): @@ -435,7 +441,7 @@ def test_bad_db_second_half(self): 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', None, ), get_libsqlite3=get_libsqlite3) as query: - with self.assertRaisesRegex(Exception, 'database disk image is malformed'): + with self.assertRaisesRegex(SQLiteError, 'database disk image is malformed'): with query("SELECT * FROM my_table") as (columns, rows): list(rows) @@ -649,7 +655,7 @@ def iter_bytes(chunk_size=None): 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', None, ), get_http_client=get_http_client, get_libsqlite3=get_libsqlite3) as query: - with self.assertRaisesRegex(Exception, 'disk I/O error'): + with self.assertRaisesRegex(SQLiteError, 'disk I/O error'): query('SELECT my_col_a FROM my_table').__enter__() def test_disconnection(self):