1
0
Fork 0
forked from s3lph/matemat

Merge branch '10-builtin-sqlite3' into 'master'

Resolve "Get rid of APSW"

Closes #10

See merge request s3lph/matemat!14
This commit is contained in:
s3lph 2018-07-14 21:47:16 +00:00
commit 2dcb8dc619
5 changed files with 50 additions and 29 deletions

View file

@ -18,7 +18,6 @@ This project intends to provide a well-tested and maintainable alternative to
- Python 3 (>=3.6) - Python 3 (>=3.6)
- Python dependencies: - Python dependencies:
- apsw
- jinja2 - jinja2
## Usage ## Usage

View file

@ -7,6 +7,7 @@ from hmac import compare_digest
from matemat.primitives import User, Product from matemat.primitives import User, Product
from matemat.exceptions import AuthenticationError, DatabaseConsistencyError from matemat.exceptions import AuthenticationError, DatabaseConsistencyError
from matemat.db import DatabaseWrapper from matemat.db import DatabaseWrapper
from matemat.db.wrapper import Transaction
# TODO: Change to METHOD_BLOWFISH when adopting Python 3.7 # TODO: Change to METHOD_BLOWFISH when adopting Python 3.7
""" """
@ -50,19 +51,19 @@ class MatematDatabase(object):
# Pass context manager stuff through to the database wrapper # Pass context manager stuff through to the database wrapper
self.db.__exit__(exc_type, exc_val, exc_tb) self.db.__exit__(exc_type, exc_val, exc_tb)
def transaction(self, exclusive: bool = True) -> Any: def transaction(self, exclusive: bool = True) -> Transaction:
""" """
Begin a new SQLite3 transaction (exclusive by default). You should never need to use the returned object (an Begin a new SQLite3 transaction (exclusive by default). You should never need to use the returned object (a
APSW cursor). It is provided in case there is a real need for it (e.g. for unit testing). Transaction instance). It is provided in case there is a real need for it (e.g. for unit testing).
This function should be used with the Context Manager 'with' syntax (PEP 343), e.g.: This function should be used with the Context Manager 'with' syntax (PEP 343), yielding a sqlite3.Cursor, e.g.:
with db.transaction(): with db.transaction():
db.foo() db.foo()
db.bar() db.bar()
:param exclusive: Whether to begin an exclusive transaction or not, defaults to True (exclusive). :param exclusive: Whether to begin an exclusive transaction or not, defaults to True (exclusive).
:return: An APSW cursor. :return: A a transaction object.
""" """
return self.db.transaction(exclusive=exclusive) return self.db.transaction(exclusive=exclusive)

View file

@ -75,3 +75,29 @@ class DatabaseTest(unittest.TestCase):
c = db._sqlite_db.cursor() c = db._sqlite_db.cursor()
c.execute("SELECT * FROM users") c.execute("SELECT * FROM users")
self.assertIsNone(c.fetchone()) self.assertIsNone(c.fetchone())
def test_connect_twice(self):
"""
If a connection is already established, a RuntimeError should be raised when attempting to connect a
second time.
"""
self.db.connect()
with self.assertRaises(RuntimeError):
self.db.connect()
self.db.close()
def test_close_not_opened(self):
"""
Attempting to close an unopened connection should raise a RuntimeError.
"""
with self.assertRaises(RuntimeError):
self.db.close()
def test_close_in_transaction(self):
"""
Attempting to close a connection inside a transaction should raise a RuntimeError.
"""
with self.db as db:
with db.transaction():
with self.assertRaises(RuntimeError):
self.db.close()

View file

@ -1,43 +1,40 @@
from typing import Any from typing import Any
import apsw import sqlite3
from matemat.exceptions import DatabaseConsistencyError from matemat.exceptions import DatabaseConsistencyError
class Transaction(object): class Transaction(object):
def __init__(self, db: apsw.Connection, wrapper: 'DatabaseWrapper', exclusive: bool = True) -> None: def __init__(self, db: sqlite3.Connection, exclusive: bool = True) -> None:
self._db: apsw.Connection = db self._db: sqlite3.Connection = db
self._cursor = None self._cursor = None
self._excl = exclusive self._excl = exclusive
self._wrapper: DatabaseWrapper = wrapper
self._is_dummy: bool = False self._is_dummy: bool = False
def __enter__(self) -> Any: def __enter__(self) -> sqlite3.Cursor:
if self._wrapper._in_transaction: if self._db.in_transaction:
self._is_dummy = True self._is_dummy = True
return self._db.cursor() return self._db.cursor()
else: else:
self._is_dummy = False self._is_dummy = False
self._cursor = self._db.cursor()
self._wrapper._in_transaction = True
if self._excl: if self._excl:
self._cursor.execute('BEGIN EXCLUSIVE') self._db.execute('BEGIN EXCLUSIVE')
else: else:
self._cursor.execute('BEGIN') self._db.execute('BEGIN')
self._cursor = self._db.cursor()
return self._cursor return self._cursor
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
if self._is_dummy: if self._is_dummy:
return return
if exc_type is None: if exc_type is None:
self._cursor.execute('COMMIT') self._db.commit()
else: else:
self._cursor.execute('ROLLBACK') self._db.rollback()
self._wrapper._in_transaction = False if exc_type == sqlite3.IntegrityError:
if exc_type == apsw.ConstraintError:
raise DatabaseConsistencyError(str(exc_val)) raise DatabaseConsistencyError(str(exc_val))
@ -47,7 +44,7 @@ class DatabaseWrapper(object):
SCHEMA = ''' SCHEMA = '''
CREATE TABLE users ( CREATE TABLE users (
user_id INTEGER PRIMARY KEY, user_id INTEGER PRIMARY KEY,
username TEXT NOT NULL, username TEXT UNIQUE NOT NULL,
email TEXT DEFAULT NULL, email TEXT DEFAULT NULL,
password TEXT NOT NULL, password TEXT NOT NULL,
touchkey TEXT DEFAULT NULL, touchkey TEXT DEFAULT NULL,
@ -72,13 +69,12 @@ class DatabaseWrapper(object):
ON DELETE CASCADE ON UPDATE CASCADE, ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(product_id) FOREIGN KEY (product_id) REFERENCES products(product_id)
ON DELETE CASCADE ON UPDATE CASCADE ON DELETE CASCADE ON UPDATE CASCADE
) );
''' '''
def __init__(self, filename: str) -> None: def __init__(self, filename: str) -> None:
self._filename: str = filename self._filename: str = filename
self._sqlite_db: apsw.Connection = None self._sqlite_db: sqlite3.Connection = None
self._in_transaction: bool = False
def __enter__(self) -> 'DatabaseWrapper': def __enter__(self) -> 'DatabaseWrapper':
self.connect() self.connect()
@ -88,13 +84,13 @@ class DatabaseWrapper(object):
self.close() self.close()
def transaction(self, exclusive: bool = True) -> Transaction: def transaction(self, exclusive: bool = True) -> Transaction:
return Transaction(self._sqlite_db, self, exclusive) return Transaction(self._sqlite_db, exclusive)
def _setup(self) -> None: def _setup(self) -> None:
with self.transaction() as c: with self.transaction() as c:
version: int = self._user_version version: int = self._user_version
if version < 1: if version < 1:
c.execute(self.SCHEMA) c.executescript(self.SCHEMA)
elif version < self.SCHEMA_VERSION: elif version < self.SCHEMA_VERSION:
self._upgrade(old=version, new=self.SCHEMA_VERSION) self._upgrade(old=version, new=self.SCHEMA_VERSION)
self._user_version = self.SCHEMA_VERSION self._user_version = self.SCHEMA_VERSION
@ -105,7 +101,7 @@ class DatabaseWrapper(object):
def connect(self) -> None: def connect(self) -> None:
if self.is_connected(): if self.is_connected():
raise RuntimeError(f'Database connection to {self._filename} is already established.') raise RuntimeError(f'Database connection to {self._filename} is already established.')
self._sqlite_db = apsw.Connection(self._filename) self._sqlite_db = sqlite3.connect(self._filename)
self._setup() self._setup()
def close(self) -> None: def close(self) -> None:
@ -117,7 +113,7 @@ class DatabaseWrapper(object):
self._sqlite_db = None self._sqlite_db = None
def in_transaction(self) -> bool: def in_transaction(self) -> bool:
return self._in_transaction return self._sqlite_db.in_transaction
def is_connected(self) -> bool: def is_connected(self) -> bool:
return self._sqlite_db is not None return self._sqlite_db is not None

View file

@ -1,2 +1 @@
apsw
jinja2 jinja2