From 7a420facc250ccb3e40e995461b1b9f790d7ab4f Mon Sep 17 00:00:00 2001 From: shouzy <82171453+realshouzy@users.noreply.github.com> Date: Mon, 13 May 2024 23:07:49 +0200 Subject: [PATCH] Implement database classes --- nrw/database/__init__.py | 1 + nrw/database/_query_result.py | 63 ++++++++++++++++ nrw/database/_query_result.pyi | 22 ++++++ nrw/database/msaccess.py | 119 ++++++++++++++++++++++++++++++ nrw/database/msaccess.pyi | 27 +++++++ nrw/database/mysql.py | 126 ++++++++++++++++++++++++++++++++ nrw/database/mysql.pyi | 28 +++++++ nrw/database/sqlite.py | 125 +++++++++++++++++++++++++++++++ nrw/database/sqlite.pyi | 27 +++++++ pyproject.toml | 14 +++- requirements-dev.txt | 1 + requirements.txt | 3 + tests/database/msaccess_test.py | 95 ++++++++++++++++++++++++ tests/database/mysql_test.py | 50 +++++++++++++ tests/database/sqlite_test.py | 44 +++++++++++ 15 files changed, 743 insertions(+), 2 deletions(-) create mode 100644 nrw/database/__init__.py create mode 100644 nrw/database/_query_result.py create mode 100644 nrw/database/_query_result.pyi create mode 100644 nrw/database/msaccess.py create mode 100644 nrw/database/msaccess.pyi create mode 100644 nrw/database/mysql.py create mode 100644 nrw/database/mysql.pyi create mode 100644 nrw/database/sqlite.py create mode 100644 nrw/database/sqlite.pyi create mode 100644 requirements.txt create mode 100644 tests/database/msaccess_test.py create mode 100644 tests/database/mysql_test.py create mode 100644 tests/database/sqlite_test.py diff --git a/nrw/database/__init__.py b/nrw/database/__init__.py new file mode 100644 index 0000000..8de3238 --- /dev/null +++ b/nrw/database/__init__.py @@ -0,0 +1 @@ +"""Die Datenbankklassen nach den Vorgaben des Landes NRW.""" diff --git a/nrw/database/_query_result.py b/nrw/database/_query_result.py new file mode 100644 index 0000000..84ffa0b --- /dev/null +++ b/nrw/database/_query_result.py @@ -0,0 +1,63 @@ +"""Klasse `QueryResult`.""" + +from __future__ import annotations + +__all__: Final[tuple[str]] = ("QueryResult",) + +from typing import Any, Final + + +class QueryResult: + """Ein Objekt der Klasse `QueryResult` stellt die Ergebnistabelle einer + Datenbankanfrage mit Hilfe + der Klasse `DatabaseConnector` dar. Objekte dieser Klasse werden nur von der + Klasse `DatabaseConnector` erstellt. + Die Klasse verfügt über keinen öffentlichen Konstruktor. + """ + + __slots__: Final[tuple[str, str, str]] = ("_data", "_column_names", "_column_types") + + def __init__( + self, + data: list[tuple[Any, ...]], + column_names: tuple[str, ...], + column_types: tuple[Any, ...], + ) -> None: + """Interner Konstruktor.""" + self._data: list[tuple[Any, ...]] = data + self._column_names: tuple[str, ...] = column_names + self._column_types: tuple[Any, ...] = column_types + + @property + def data(self) -> list[tuple[Any, ...]]: + """Die Anfrage liefert die Einträge der Ergebnistabelle als eine `list` + welche wiederum `tuple` enthält. Der erste Index stellt die Zeile und der zweite + die Spalte dar (d.h. Object[zeile][spalte]). + """ + return self._data + + @property + def column_names(self) -> tuple[str, ...]: + """Die Anfrage liefert die Bezeichner der Spalten der Ergebnistabelle als + `tuple` vom Typ `str` zurück. + """ + return self._column_names + + @property + def column_types(self) -> tuple[Any, ...]: + """Die Anfrage liefert (wenn möglich) die Typenbezeichnung der Spalten der + Ergebnistabelle als `tuple` vom jeweiligen Typ zurück. + Die Bezeichnungen entsprechen den Angaben in der `MySQL`-Datenbank. + """ + return self._column_types + + @property + def row_count(self) -> int: + """Die Anfrage liefert die Anzahl der Zeilen der Ergebnistabelle als `int`.""" + return len(self._data) if self._data is not None else 0 + + @property + def column_count(self) -> int: + """Die Anfrage liefert die Anzahl der Spalten der Ergebnistabelle als `int`.""" + assert all(len(self._data[0]) == len(data) for data in self._data[1:]) + return len(self._data[0]) diff --git a/nrw/database/_query_result.pyi b/nrw/database/_query_result.pyi new file mode 100644 index 0000000..9f5089b --- /dev/null +++ b/nrw/database/_query_result.pyi @@ -0,0 +1,22 @@ +__all__: Final[tuple[str]] = ("QueryResult",) + +from typing import Any, Final + +class QueryResult: + __slots__: Final[tuple[str, str, str]] = ("_data", "_column_names", "_column_types") + def __init__( + self, + data: list[tuple[Any, ...]], + column_names: tuple[str, ...], + column_types: tuple[Any, ...], + ) -> None: ... + @property + def data(self) -> list[tuple[Any, ...]]: ... + @property + def column_names(self) -> tuple[str, ...]: ... + @property + def column_types(self) -> tuple[Any, ...]: ... + @property + def row_count(self) -> int: ... + @property + def column_count(self) -> int: ... diff --git a/nrw/database/msaccess.py b/nrw/database/msaccess.py new file mode 100644 index 0000000..a49bb1c --- /dev/null +++ b/nrw/database/msaccess.py @@ -0,0 +1,119 @@ +"""Klasse `DatabaseConnector`.""" + +from __future__ import annotations + +__all__: Final[tuple[str]] = ("DatabaseConnector",) + +from typing import Final + +import pyodbc # type: ignore[import-not-found] + +from nrw.database._query_result import QueryResult + + +class DatabaseConnector: + """Ein Objekt der Klasse `DatabaseConnector` ermöglicht die Abfrage und Manipulation + einer `MSAccess`-Datenbank. + Beim Erzeugen des Objekts wird eine Datenbankverbindung aufgebaut, so dass + anschließend SQL-Anweisungen an diese Datenbank gerichtet werden können. + """ + + __slots__: Final[tuple[str, str, str]] = ( + "_connection", + "_current_query_result", + "_message", + ) + + # pylint: disable=W0613, W0718 + + def __init__( + self, + ip: None, + port: None, + database: str, + username: None, + password: None, + ) -> None: + """Ein Objekt vom Typ `DatabaseConnector` wird erstellt, und eine Verbindung zur + Datenbank wird aufgebaut. Mit den Parametern `ip` und `port` werden die + IP-Adresse und die Port-Nummer übergeben, unter denen die Datenbank mit Namen + `database` zu erreichen ist. + Mit den Parametern `username` und `password` werden Benutzername und Passwort + für die Datenbank übergeben. + + Für `MSAccess` wird nur `database` benötigt. + """ + self._current_query_result: QueryResult | None = None + self._message: str | None = None + + try: + self._connection = pyodbc.connect( + f"Driver={{Microsoft Access Driver (*.mdb, *.accdb)}};DBQ={database};", + autocommit=True, + ) + except Exception as exception: + self._connection = None + self._message = str(exception) + + def execute_statement(self, sql_statement: str) -> None: + """Der Auftrag schickt den im Parameter `sql_statement` enthaltenen SQL-Befehl + an die Datenbank ab. + Handelt es sich bei `sql_statement` um einen SQL-Befehl, der eine Ergebnismenge + liefert, so kann dieses Ergebnis anschließend mit dem Property + `current_query_result` abgerufen werden. + """ + self._current_query_result = None + self._message = None + + if self._connection is None: + self._message = "No connection" + return + + try: + with self._connection.cursor() as cursor: + cursor.execute(sql_statement) + if data := cursor.fetchall(): + assert cursor.description is not None, "No description" + column_names: tuple[str, ...] = tuple( + column[0] for column in cursor.description + ) + colum_types: tuple[int, ...] = tuple( + column[1] for column in cursor.description + ) + self._current_query_result = QueryResult( + [tuple(row) for row in data], + column_names, + colum_types, + ) + except Exception as exception: + if str(exception) != "No results. Previous SQL was not a query.": + self._message = str(exception) + + @property + def current_query_result(self) -> QueryResult | None: + """Die Anfrage liefert das Ergebnis des letzten mit der Methode + `execute_statement` an die Datenbank geschickten SQL-Befehls als Objekt vom Typ + `QueryResult` zurück. + Wurde bisher kein SQL-Befehl abgeschickt oder ergab der letzte Aufruf von + `execute_statement` keine Ergebnismenge (z.B. bei einem INSERT-Befehl oder einem + Syntaxfehler), so wird `None` geliefert. + """ + return self._current_query_result + + @property + def error_message(self) -> str | None: + """Die Anfrage liefert `None` oder eine Fehlermeldung, die sich jeweils auf die + letzte zuvor ausgefuehrte Datenbankoperation bezieht. + """ + return self._message + + def close(self) -> None: + """Die Datenbankverbindung wird geschlossen.""" + if self._connection is None: + self._message = "No connection" + return + + try: + self._connection.close() + except Exception as exception: + self._message = str(exception) diff --git a/nrw/database/msaccess.pyi b/nrw/database/msaccess.pyi new file mode 100644 index 0000000..6cd98f5 --- /dev/null +++ b/nrw/database/msaccess.pyi @@ -0,0 +1,27 @@ +__all__: Final[tuple[str]] = ("DatabaseConnector",) + +from typing import Final + +from nrw.database._query_result import QueryResult + +class DatabaseConnector: + __slots__: Final[tuple[str, str, str]] = ( + "_connection", + "_current_query_result", + "_message", + ) + + def __init__( + self, + ip: None, + port: None, + database: str, + username: None, + password: None, + ) -> None: ... + def execute_statement(self, sql_statement: str) -> None: ... + @property + def current_query_result(self) -> QueryResult | None: ... + @property + def error_message(self) -> str | None: ... + def close(self) -> None: ... diff --git a/nrw/database/mysql.py b/nrw/database/mysql.py new file mode 100644 index 0000000..25b66a0 --- /dev/null +++ b/nrw/database/mysql.py @@ -0,0 +1,126 @@ +"""Klasse `DatabaseConnector`.""" + +from __future__ import annotations + +__all__: Final[tuple[str]] = ("DatabaseConnector",) + +from typing import TYPE_CHECKING, Final + +import mysql.connector + +from nrw.database._query_result import QueryResult + +if TYPE_CHECKING: + from mysql.connector.abstracts import MySQLConnectionAbstract + from mysql.connector.pooling import PooledMySQLConnection + + +class DatabaseConnector: + """Ein Objekt der Klasse `DatabaseConnector` ermöglicht die Abfrage und Manipulation + einer `MySQL`-Datenbank. + Beim Erzeugen des Objekts wird eine Datenbankverbindung aufgebaut, so dass + anschließend SQL-Anweisungen an diese Datenbank gerichtet werden können. + """ + + # pylint: disable=W0613, W0718, R0913 + + __slots__: Final[tuple[str, str, str]] = ( + "_connection", + "_current_query_result", + "_message", + ) + + def __init__( + self, + ip: str, + port: int, + database: str, + username: str, + password: str, + ) -> None: + """Ein Objekt vom Typ `DatabaseConnector` wird erstellt, und eine Verbindung zur + Datenbank wird aufgebaut. Mit den Parametern `ip` und `port` werden die + IP-Adresse und die Port-Nummer übergeben, unter denen die Datenbank mit Namen + `database` zu erreichen ist. + Mit den Parametern `username` und `password` werden Benutzername und Passwort + für die Datenbank übergeben. + """ + self._current_query_result: QueryResult | None = None + self._message: str | None = None + + try: + self._connection: PooledMySQLConnection | MySQLConnectionAbstract | None = ( + mysql.connector.connect( + user=username, + password=password, + host=ip, + port=port, + database=database, + autocommit=True, + ) + ) + except Exception as exception: + self._connection = None + self._message = str(exception) + + def execute_statement(self, sql_statement: str) -> None: + """Der Auftrag schickt den im Parameter `sql_statement` enthaltenen SQL-Befehl + an die Datenbank ab. + Handelt es sich bei `sql_statement` um einen SQL-Befehl, der eine Ergebnismenge + liefert, so kann dieses Ergebnis anschließend mit dem Property + `current_query_result` abgerufen werden. + """ + self._current_query_result = None + self._message = None + + if self._connection is None: + self._message = "No connection" + return + + try: + with self._connection.cursor(dictionary=False) as cursor: + cursor.execute(sql_statement) + if data := cursor.fetchall(): + assert cursor.description is not None, "No description" + column_names: tuple[str, ...] = tuple( + column[0] for column in cursor.description + ) + colum_types: tuple[int, ...] = tuple( + column[1] for column in cursor.description + ) + self._current_query_result = QueryResult( + data, # type: ignore[arg-type] + column_names, + colum_types, + ) + except Exception as exception: + self._message = str(exception) + + @property + def current_query_result(self) -> QueryResult | None: + """Die Anfrage liefert das Ergebnis des letzten mit der Methode + `execute_statement` an die Datenbank geschickten SQL-Befehls als Objekt vom Typ + `QueryResult` zurück. + Wurde bisher kein SQL-Befehl abgeschickt oder ergab der letzte Aufruf von + `execute_statement` keine Ergebnismenge (z.B. bei einem INSERT-Befehl oder einem + Syntaxfehler), so wird `None` geliefert. + """ + return self._current_query_result + + @property + def error_message(self) -> str | None: + """Die Anfrage liefert `None` oder eine Fehlermeldung, die sich jeweils auf die + letzte zuvor ausgefuehrte Datenbankoperation bezieht. + """ + return self._message + + def close(self) -> None: + """Die Datenbankverbindung wird geschlossen.""" + if self._connection is None: + self._message = "No connection" + return + + try: + self._connection.close() + except Exception as exception: + self._message = str(exception) diff --git a/nrw/database/mysql.pyi b/nrw/database/mysql.pyi new file mode 100644 index 0000000..8bde8ae --- /dev/null +++ b/nrw/database/mysql.pyi @@ -0,0 +1,28 @@ +__all__: Final[tuple[str]] = ("DatabaseConnector",) + +from typing import Final + +from nrw.database._query_result import QueryResult + +class DatabaseConnector: + + __slots__: Final[tuple[str, str, str]] = ( + "_connection", + "_current_query_result", + "_message", + ) + + def __init__( + self, + ip: str, + port: int, + database: str, + username: str, + password: str, + ) -> None: ... + def execute_statement(self, sql_statement: str) -> None: ... + @property + def current_query_result(self) -> QueryResult | None: ... + @property + def error_message(self) -> str | None: ... + def close(self) -> None: ... diff --git a/nrw/database/sqlite.py b/nrw/database/sqlite.py new file mode 100644 index 0000000..aeec3f3 --- /dev/null +++ b/nrw/database/sqlite.py @@ -0,0 +1,125 @@ +"""Klasse `DatabaseConnector`.""" + +from __future__ import annotations + +__all__: Final[tuple[str]] = ("DatabaseConnector",) + +import sqlite3 +from typing import Final + +from nrw.database._query_result import QueryResult + + +class DatabaseConnector: + """Ein Objekt der Klasse `DatabaseConnector` ermöglicht die Abfrage und + Manipulation einer `SQLite`-Datenbank. + Beim Erzeugen des Objekts wird eine Datenbankverbindung aufgebaut, so dass + anschließend SQL-Anweisungen an diese Datenbank gerichtet werden koennen. + """ + + # pylint: disable=W0613, W0718, R0913 + + __slots__: Final[tuple[str, str, str]] = ( + "_connection", + "_current_query_result", + "_message", + ) + + def __init__( + self, + ip: None, + port: None, + database: str, + username: None, + password: None, + ) -> None: + """Ein Objekt vom Typ `DatabaseConnector` wird erstellt, und eine Verbindung zur + Datenbank wird aufgebaut. Mit den Parametern `ip` und `port` werden die + IP-Adresse und die Port-Nummer übergeben, unter denen die Datenbank mit Namen + `database` zu erreichen ist. + Mit den Parametern `username` und `password` werden Benutzername und Passwort + für die Datenbank übergeben. + + Für `SQLite` wird nur `database` benötigt. + """ + self._current_query_result: QueryResult | None = None + self._message: str | None = None + + try: + self._connection: sqlite3.Connection | None = sqlite3.connect( + database, + autocommit=True, + ) # type: ignore[call-arg] + except Exception as exception: + self._connection = None + self._message = str(exception) + + def execute_statement(self, sql_statement: str) -> None: + """Der Auftrag schickt den im Parameter `sql_statement` enthaltenen SQL-Befehl + an die Datenbank ab. + Handelt es sich bei `sql_statement` um einen SQL-Befehl, der eine Ergebnismenge + liefert, so kann dieses Ergebnis anschließend mit dem Property + `current_query_result` abgerufen werden. + """ + self._current_query_result = None + self._message = None + + if self._connection is None: + self._message = "No connection" + return + + try: + cursor: sqlite3.Cursor = self._connection.cursor() + except Exception as exception: + self._message = str(exception) + return + + try: + if cursor.execute(sql_statement) is not None and ( + data := cursor.fetchall() + ): + assert cursor.description is not None, "No description" + column_names: tuple[str, ...] = tuple( + column[0] for column in cursor.description + ) + colum_types: tuple[None, ...] = tuple( + column[1] for column in cursor.description + ) + self._current_query_result = QueryResult( + data, + column_names, + colum_types, + ) + except Exception as exception: + self._message = str(exception) + finally: + cursor.close() + + @property + def current_query_result(self) -> QueryResult | None: + """Die Anfrage liefert das Ergebnis des letzten mit der Methode + `execute_statement` an die Datenbank geschickten SQL-Befehls als Objekt vom Typ + `QueryResult` zurück. + Wurde bisher kein SQL-Befehl abgeschickt oder ergab der letzte Aufruf von + `execute_statement` keine Ergebnismenge (z.B. bei einem INSERT-Befehl oder einem + Syntaxfehler), so wird `None` geliefert. + """ + return self._current_query_result + + @property + def error_message(self) -> str | None: + """Die Anfrage liefert `None` oder eine Fehlermeldung, die sich jeweils auf die + letzte zuvor ausgefuehrte Datenbankoperation bezieht. + """ + return self._message + + def close(self) -> None: + """Die Datenbankverbindung wird geschlossen.""" + if self._connection is None: + self._message = "No connection" + return + + try: + self._connection.close() + except Exception as exception: + self._message = str(exception) diff --git a/nrw/database/sqlite.pyi b/nrw/database/sqlite.pyi new file mode 100644 index 0000000..6cd98f5 --- /dev/null +++ b/nrw/database/sqlite.pyi @@ -0,0 +1,27 @@ +__all__: Final[tuple[str]] = ("DatabaseConnector",) + +from typing import Final + +from nrw.database._query_result import QueryResult + +class DatabaseConnector: + __slots__: Final[tuple[str, str, str]] = ( + "_connection", + "_current_query_result", + "_message", + ) + + def __init__( + self, + ip: None, + port: None, + database: str, + username: None, + password: None, + ) -> None: ... + def execute_statement(self, sql_statement: str) -> None: ... + @property + def current_query_result(self) -> QueryResult | None: ... + @property + def error_message(self) -> str | None: ... + def close(self) -> None: ... diff --git a/pyproject.toml b/pyproject.toml index c168c41..c3d129d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ classifiers = [ "Operating System :: OS Independent", ] requires-python = ">=3.8" -dynamic = ["version"] +dynamic = ["version", "dependencies"] [project.optional-dependencies] # keep in sync with requirements-dev.txt @@ -41,6 +41,7 @@ dev = [ "covdefaults", "coverage", "isort", + "git+https://github.com/realshouzy/msaccessdb", "mypy", "pre-commit", "pylint", @@ -59,6 +60,7 @@ datastructures = ["py.typed", "*.pyi"] [tool.setuptools.dynamic] version = { attr = "nrw.__version__" } +dependencies = { file = "requirements.txt" } [tool.black] target-version = ["py312", "py311", "py310", "py39", "py38"] @@ -119,6 +121,7 @@ line-length = 88 [tool.ruff.lint.extend-per-file-ignores] "./tests/*_test.py" = ["SLF001", "D100", "D103", "PLR2004"] "./tests/*.py" = ["D104"] +"./nrw/database/*.py" = ["BLE001", "PLR0913", "ARG002"] [tool.ruff.lint.isort] known-first-party = ["nrw"] @@ -128,7 +131,7 @@ required-imports = ["from __future__ import annotations"] convention = "pep257" [tool.pylint] -disable = ["C0116", "R0801", "W0212", "R0903", "C0114"] +disable = ["C0116", "R0801", "W0212", "R0903", "C0114", "I1101"] load-plugins = "pylint_pytest" [tool.bandit] @@ -140,4 +143,11 @@ testpaths = ["tests"] python_files = ["*_test.py"] [tool.coverage.run] +omit = ["./tests/*"] plugins = ["covdefaults"] + +[tool.coverage.report] +exclude_also = [ + "except Exception as exception:", + "if self._connection is None:", +] diff --git a/requirements-dev.txt b/requirements-dev.txt index a950ccd..d76131d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,6 +4,7 @@ black covdefaults coverage isort +git+https://github.com/realshouzy/msaccessdb mypy pre-commit pylint diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c481e82 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +mysql-connector-python +pyodbc;platform_python_implementation=="CPython" +pypyodbc;platform_python_implementation=="PyPy" diff --git a/tests/database/msaccess_test.py b/tests/database/msaccess_test.py new file mode 100644 index 0000000..8a14f54 --- /dev/null +++ b/tests/database/msaccess_test.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Tests for `database.msaccess`.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Iterator + +import msaccessdb +import pyodbc # type: ignore[import-not-found] +import pytest + +from nrw.database.msaccess import DatabaseConnector + +if TYPE_CHECKING: + from pathlib import Path + + +@pytest.fixture() +def msaccess_db(tmp_path: Path) -> Iterator[str]: + test_db: Path = tmp_path / "test.accdb" + test_db.touch() + try: + msaccessdb.create(test_db) + yield str(test_db) + finally: + test_db.unlink() + + +@pytest.mark.skipif( + not any( + driver.startswith("Microsoft Access Driver") for driver in pyodbc.drivers() + ), + reason="No Microsoft Access Driver available", +) +def test_database_connector_with_driver( + msaccess_db: str, +) -> None: # pragma: no branch + db: DatabaseConnector = DatabaseConnector( + None, + None, + msaccess_db, + None, + None, + ) + assert db.error_message is None + assert db.current_query_result is None + + db.execute_statement("create table test (a Int, b Text, c Text);") + assert db.error_message is None + assert db.current_query_result is None + + db.execute_statement("insert into test values (1, 'hello', 'world');") + assert db.error_message is None + assert db.current_query_result is None + + db.execute_statement("insert into test values (2, 'test', 'test');") + assert db.error_message is None + assert db.current_query_result is None + + db.execute_statement("select * from test;") + assert db.error_message is None + assert db.current_query_result is not None + assert db.current_query_result.data == [(1, "hello", "world"), (2, "test", "test")] + assert db.current_query_result.column_names == ("a", "b", "c") + assert db.current_query_result.column_types == (int, str, str) + assert db.current_query_result.row_count == 2 + assert db.current_query_result.column_count == 3 + + db.close() + assert db.error_message is None + + +@pytest.mark.skipif( + any(driver.startswith("Microsoft Access Driver") for driver in pyodbc.drivers()), + reason="Microsoft Access Driver available", +) +def test_database_connector_without_driver( + msaccess_db: str, +) -> None: # pragma: no branch + db: DatabaseConnector = DatabaseConnector( + None, + None, + msaccess_db, + None, + None, + ) + assert db.error_message is not None + assert db.current_query_result is None + + db.close() + assert db.error_message is not None + assert db.current_query_result is None + + +if __name__ == "__main__": + raise SystemExit(pytest.main()) diff --git a/tests/database/mysql_test.py b/tests/database/mysql_test.py new file mode 100644 index 0000000..95d6256 --- /dev/null +++ b/tests/database/mysql_test.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +"""Tests for `database.mysql`.""" +from __future__ import annotations + +import pytest + +from nrw.database.mysql import DatabaseConnector + + +def test_database_connector() -> None: + db: DatabaseConnector = DatabaseConnector("127.0.0.1", 3306, "test", "test", "test") + assert db.error_message is None + assert db.current_query_result is None + + db.execute_statement("drop table test;") + assert db.error_message is None + assert db.current_query_result is None + + db.execute_statement("create table test (a Int, b Text, c Text);") + assert db.error_message is None + assert db.current_query_result is None + + db.execute_statement("insert into test values (1, 'hello', 'world');") + assert db.error_message is None + assert db.current_query_result is None + + db.execute_statement("insert into test values (2, 'test', 'test');") + assert db.error_message is None + assert db.current_query_result is None + + db.execute_statement("select * from test;") + assert db.error_message is None + assert db.current_query_result is not None + assert db.current_query_result.data == [ + (1, "hello", "world"), + (2, "test", "test"), + ] + assert db.current_query_result.column_names == ("a", "b", "c") + assert db.current_query_result.column_types == (3, 252, 252) + assert db.current_query_result.row_count == 2 + assert db.current_query_result.column_count == 3 + + db.close() + assert db.error_message is None + + +if __name__ == "__main__": + raise SystemExit(pytest.main()) + raise SystemExit(pytest.main()) + raise SystemExit(pytest.main()) diff --git a/tests/database/sqlite_test.py b/tests/database/sqlite_test.py new file mode 100644 index 0000000..3830976 --- /dev/null +++ b/tests/database/sqlite_test.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +"""Tests for `database.sqlite`.""" +from __future__ import annotations + +import pytest + +from nrw.database.sqlite import DatabaseConnector + + +def test_database_connector() -> None: + db: DatabaseConnector = DatabaseConnector(None, None, ":memory:", None, None) + assert db.error_message is None + assert db.current_query_result is None + + db.execute_statement("create table test (a Int, b Text, c Text);") + assert db.error_message is None + assert db.current_query_result is None + + db.execute_statement("insert into test values (1, 'hello', 'world');") + assert db.error_message is None + assert db.current_query_result is None + + db.execute_statement("insert into test values (2, 'test', 'test');") + assert db.error_message is None + assert db.current_query_result is None + + db.execute_statement("select * from test;") + assert db.error_message is None + assert db.current_query_result is not None + assert db.current_query_result.data == [ + (1, "hello", "world"), + (2, "test", "test"), + ] + assert db.current_query_result.column_names == ("a", "b", "c") + assert db.current_query_result.column_types == (None, None, None) + assert db.current_query_result.row_count == 2 + assert db.current_query_result.column_count == 3 + + db.close() + assert db.error_message is None + + +if __name__ == "__main__": + raise SystemExit(pytest.main())