Skip to content

Commit 58d06b4

Browse files
authored
Merge pull request #90 from michalc/feat/raise-httpx-exceptions
feat: raise any httpx exceptions, e.g. from failed auth
2 parents 92267ea + 0a836ef commit 58d06b4

File tree

3 files changed

+93
-4
lines changed

3 files changed

+93
-4
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,13 @@ This means that sqlite-s3-query is not for all use cases of querying SQLite data
238238
This is not necessarily a permanent decision - it is possible that in future sqlite-s3-query will support unversioned buckets.
239239

240240

241-
## Exception hierarchy
241+
## Exceptions
242+
243+
Under the hood [HTTPX](https://www.python-httpx.org/) is used to communicate with S3, but any [exceptions raised by HTTPX](https://www.python-httpx.org/exceptions/) are passed through to client code unchanged. This includes `httpx.HTTPStatusError` when S3 returns a non-200 response. Most commonly this will be when S3 returns a 403 in the case of insufficient permissions on the database object being queried.
244+
245+
All other exceptions raised inherit from `sqlite_s3_query.SQLiteS3QueryError` as described in the following hierarchy.
246+
247+
### Exception hierarchy
242248

243249
- `SQLiteS3QueryError`
244250

sqlite_s3_query.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import hmac
22
import os
3+
import threading
34
from contextlib import contextmanager
45
from ctypes import CFUNCTYPE, POINTER, Structure, create_string_buffer, pointer, cast, memmove, memset, sizeof, addressof, cdll, byref, string_at, c_char_p, c_int, c_double, c_int64, c_void_p, c_char
56
from ctypes.util import find_library
@@ -69,13 +70,33 @@ def sqlite_s3_query_multi(url, get_credentials=lambda now: (
6970
body_hash = sha256(b'').hexdigest()
7071
scheme, netloc, path, _, _ = urlsplit(url)
7172

73+
# We could use contextvars, but they aren't introduced until Python 3.7
74+
pending_exceptions = {}
75+
pending_exception_lock = threading.Lock()
76+
77+
def set_pending_exception(exception):
78+
thread_id = threading.get_ident()
79+
with pending_exception_lock:
80+
pending_exceptions[thread_id] = exception
81+
82+
def raise_any_pending_exception():
83+
thread_id = threading.get_ident()
84+
with pending_exception_lock:
85+
try:
86+
raise pending_exceptions.pop(thread_id)
87+
except KeyError:
88+
pass
89+
7290
def run(func, *args):
7391
res = func(*args)
92+
raise_any_pending_exception()
7493
if res != 0:
7594
raise SQLiteError(libsqlite3.sqlite3_errstr(res).decode())
7695

7796
def run_with_db(db, func, *args):
78-
if func(*args) != 0:
97+
res = func(*args)
98+
raise_any_pending_exception()
99+
if res != 0:
79100
raise SQLiteError(libsqlite3.sqlite3_errmsg(db).decode())
80101

81102
@contextmanager
@@ -183,7 +204,8 @@ def x_read(p_file, p_out, i_amt, i_ofst):
183204
offset += len(chunk)
184205
if offset > i_amt:
185206
break
186-
except Exception:
207+
except Exception as exception:
208+
set_pending_exception(exception)
187209
return SQLITE_IOERR
188210

189211
if offset != i_amt:
@@ -328,6 +350,7 @@ def rows(get_pp_stmt, columns):
328350
if res == SQLITE_DONE:
329351
break
330352
if res != SQLITE_ROW:
353+
raise_any_pending_exception()
331354
raise SQLiteError(libsqlite3.sqlite3_errstr(res).decode())
332355

333356
yield tuple(

test.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import uuid
1616

1717
import httpx
18+
from httpx import HTTPStatusError
1819

1920
from sqlite_s3_query import (
2021
VersioningNotEnabledError,
@@ -411,9 +412,56 @@ def test_empty_object(self):
411412
'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
412413
None,
413414
), get_libsqlite3=get_libsqlite3) as query:
414-
with self.assertRaisesRegex(SQLiteError, 'disk I/O error'):
415+
with self.assertRaisesRegex(HTTPStatusError, r"\b416\b"):
415416
query('SELECT 1').__enter__()
416417

418+
def test_incorrect_permission_on_context_enter(self):
419+
with get_db([("CREATE TABLE my_table (my_col_a text, my_col_b text);",())]) as db:
420+
put_object_with_versioning('my-bucket', 'my.db', db)
421+
422+
with self.assertRaisesRegex(HTTPStatusError, r"\b403\b"):
423+
sqlite_s3_query('http://localhost:9000/my-bucket/my.db', get_credentials=lambda now: (
424+
'us-east-1',
425+
'AKIAIOSFODNN7EXAMPLE',
426+
'not-the-right-key',
427+
None,
428+
), get_libsqlite3=get_libsqlite3).__enter__()
429+
430+
def test_incorrect_permission_on_run_query(self):
431+
with get_db([("CREATE TABLE my_table (my_col_a text, my_col_b text);",())]) as db:
432+
put_object_with_versioning('my-bucket', 'my.db', db)
433+
434+
creds = (
435+
(
436+
'us-east-1',
437+
'AKIAIOSFODNN7EXAMPLE',
438+
'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
439+
None,
440+
), (
441+
'us-east-1',
442+
'AKIAIOSFODNN7EXAMPLE',
443+
'not-the-right-key',
444+
None,
445+
)
446+
)
447+
creds_it = iter(creds)
448+
449+
with sqlite_s3_query('http://localhost:9000/my-bucket/my.db', get_credentials=lambda now: next(creds_it), get_libsqlite3=get_libsqlite3) as query:
450+
with self.assertRaisesRegex(HTTPStatusError, r"\b403\b"):
451+
query('SELECT 1').__enter__()
452+
453+
def test_short_db_header(self):
454+
put_object_with_versioning('my-bucket', 'my.db', lambda: (b'*' * 99,))
455+
456+
with sqlite_s3_query('http://localhost:9000/my-bucket/my.db', get_credentials=lambda now: (
457+
'us-east-1',
458+
'AKIAIOSFODNN7EXAMPLE',
459+
'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
460+
None,
461+
), get_libsqlite3=get_libsqlite3) as query:
462+
with self.assertRaisesRegex(SQLiteError, 'disk I/O error'):
463+
query("SELECT * FROM non_table").__enter__()
464+
417465
def test_bad_db_header(self):
418466
put_object_with_versioning('my-bucket', 'my.db', lambda: (b'*' * 100,))
419467

@@ -426,6 +474,18 @@ def test_bad_db_header(self):
426474
with self.assertRaisesRegex(SQLiteError, 'disk I/O error'):
427475
query("SELECT * FROM non_table").__enter__()
428476

477+
def test_bad_db_first_page(self):
478+
put_object_with_versioning('my-bucket', 'my.db', lambda: (b'*' * 4096,))
479+
480+
with sqlite_s3_query('http://localhost:9000/my-bucket/my.db', get_credentials=lambda now: (
481+
'us-east-1',
482+
'AKIAIOSFODNN7EXAMPLE',
483+
'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
484+
None,
485+
), get_libsqlite3=get_libsqlite3) as query:
486+
with self.assertRaisesRegex(SQLiteError, 'not a database'):
487+
query("SELECT * FROM non_table").__enter__()
488+
429489
def test_bad_db_second_half(self):
430490
with get_db([("CREATE TABLE my_table (my_col_a text, my_col_b text);",())] + [
431491
("INSERT INTO my_table VALUES " + ','.join(["('some-text-a', 'some-text-b')"] * 500),()),

0 commit comments

Comments
 (0)