diff --git a/README.md b/README.md index b27f921..95aa201 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,6 @@ This project intends to provide a well-tested and maintainable alternative to - Python 3 (>=3.6) - Python dependencies: - - apsw - jinja2 ## Usage diff --git a/matemat/db/facade.py b/matemat/db/facade.py index 106c792..8dd8e93 100644 --- a/matemat/db/facade.py +++ b/matemat/db/facade.py @@ -7,6 +7,7 @@ from hmac import compare_digest from matemat.primitives import User, Product from matemat.exceptions import AuthenticationError, DatabaseConsistencyError from matemat.db import DatabaseWrapper +from matemat.db.wrapper import Transaction # 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 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 - APSW cursor). It is provided in case there is a real need for it (e.g. for unit testing). + Begin a new SQLite3 transaction (exclusive by default). You should never need to use the returned object (a + 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(): db.foo() db.bar() :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) diff --git a/matemat/db/test/test_wrapper.py b/matemat/db/test/test_wrapper.py index dafff65..e5a52c2 100644 --- a/matemat/db/test/test_wrapper.py +++ b/matemat/db/test/test_wrapper.py @@ -75,3 +75,29 @@ class DatabaseTest(unittest.TestCase): c = db._sqlite_db.cursor() c.execute("SELECT * FROM users") 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() diff --git a/matemat/db/wrapper.py b/matemat/db/wrapper.py index 509dade..e930d17 100644 --- a/matemat/db/wrapper.py +++ b/matemat/db/wrapper.py @@ -1,43 +1,40 @@ from typing import Any -import apsw +import sqlite3 from matemat.exceptions import DatabaseConsistencyError class Transaction(object): - def __init__(self, db: apsw.Connection, wrapper: 'DatabaseWrapper', exclusive: bool = True) -> None: - self._db: apsw.Connection = db + def __init__(self, db: sqlite3.Connection, exclusive: bool = True) -> None: + self._db: sqlite3.Connection = db self._cursor = None self._excl = exclusive - self._wrapper: DatabaseWrapper = wrapper self._is_dummy: bool = False - def __enter__(self) -> Any: - if self._wrapper._in_transaction: + def __enter__(self) -> sqlite3.Cursor: + if self._db.in_transaction: self._is_dummy = True return self._db.cursor() else: self._is_dummy = False - self._cursor = self._db.cursor() - self._wrapper._in_transaction = True if self._excl: - self._cursor.execute('BEGIN EXCLUSIVE') + self._db.execute('BEGIN EXCLUSIVE') else: - self._cursor.execute('BEGIN') + self._db.execute('BEGIN') + self._cursor = self._db.cursor() return self._cursor def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: if self._is_dummy: return if exc_type is None: - self._cursor.execute('COMMIT') + self._db.commit() else: - self._cursor.execute('ROLLBACK') - self._wrapper._in_transaction = False - if exc_type == apsw.ConstraintError: + self._db.rollback() + if exc_type == sqlite3.IntegrityError: raise DatabaseConsistencyError(str(exc_val)) @@ -47,7 +44,7 @@ class DatabaseWrapper(object): SCHEMA = ''' CREATE TABLE users ( user_id INTEGER PRIMARY KEY, - username TEXT NOT NULL, + username TEXT UNIQUE NOT NULL, email TEXT DEFAULT NULL, password TEXT NOT NULL, touchkey TEXT DEFAULT NULL, @@ -72,13 +69,12 @@ class DatabaseWrapper(object): ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (product_id) REFERENCES products(product_id) ON DELETE CASCADE ON UPDATE CASCADE - ) + ); ''' def __init__(self, filename: str) -> None: self._filename: str = filename - self._sqlite_db: apsw.Connection = None - self._in_transaction: bool = False + self._sqlite_db: sqlite3.Connection = None def __enter__(self) -> 'DatabaseWrapper': self.connect() @@ -88,13 +84,13 @@ class DatabaseWrapper(object): self.close() def transaction(self, exclusive: bool = True) -> Transaction: - return Transaction(self._sqlite_db, self, exclusive) + return Transaction(self._sqlite_db, exclusive) def _setup(self) -> None: with self.transaction() as c: version: int = self._user_version if version < 1: - c.execute(self.SCHEMA) + c.executescript(self.SCHEMA) elif version < self.SCHEMA_VERSION: self._upgrade(old=version, new=self.SCHEMA_VERSION) self._user_version = self.SCHEMA_VERSION @@ -105,7 +101,7 @@ class DatabaseWrapper(object): def connect(self) -> None: if self.is_connected(): 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() def close(self) -> None: @@ -117,7 +113,7 @@ class DatabaseWrapper(object): self._sqlite_db = None def in_transaction(self) -> bool: - return self._in_transaction + return self._sqlite_db.in_transaction def is_connected(self) -> bool: return self._sqlite_db is not None diff --git a/requirements.txt b/requirements.txt index 7663204..7f7afbf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ -apsw jinja2