diff --git a/pyproject.toml b/pyproject.toml index 6356117..f9c5d33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "teilchensammler-cli" -version = "0.5.0" +version = "0.5.1" description = "Build up and maintain an inventory of electronics parts and tools." readme = "README.md" requires-python = ">=3.14,<4.0" diff --git a/src/teilchensammler_cli/database.py b/src/teilchensammler_cli/database.py index b58456b..2d9f888 100644 --- a/src/teilchensammler_cli/database.py +++ b/src/teilchensammler_cli/database.py @@ -6,5 +6,5 @@ sqlite_url = os.environ.get("DATABASE_URL", "sqlite:///database.db") engine = create_engine(sqlite_url, echo=False) -def create_db_and_tables(): +def create_db_and_tables(engine): SQLModel.metadata.create_all(engine) diff --git a/src/teilchensammler_cli/main.py b/src/teilchensammler_cli/main.py index 42feda2..9787807 100644 --- a/src/teilchensammler_cli/main.py +++ b/src/teilchensammler_cli/main.py @@ -12,7 +12,7 @@ from textual.screen import Screen from textual.widget import Widget from textual.widgets import Button, DataTable, Footer, Header, Input, Static -from .database import create_db_and_tables +from .database import create_db_and_tables, engine from .models import add_to_database, load_initial_data, make_teilchen_input logging.basicConfig( @@ -26,7 +26,7 @@ TEILCHEN_DATA_HEADER = "pk Name Description Number Tags".split() class SammlerApp(App): async def on_mount(self) -> None: - create_db_and_tables() + create_db_and_tables(engine) self.push_screen(AddInventoryScreen()) @@ -68,7 +68,7 @@ class SearchBar(Static): event.input.value = "" - teilchen = await add_to_database(tc) + teilchen = await add_to_database(tc, engine) self.screen.query_one(DataTable).add_row( teilchen.id, teilchen.name, teilchen.description, teilchen.number, teilchen.tags ) @@ -81,7 +81,7 @@ class SearchResults(Widget): async def on_mount(self) -> None: table: DataTable = self.query_one(DataTable) table.add_columns(*TEILCHEN_DATA_HEADER) - table.add_rows(await load_initial_data()) + table.add_rows(await load_initial_data(engine)) class AddInventoryScreen(Screen): diff --git a/src/teilchensammler_cli/models.py b/src/teilchensammler_cli/models.py index bca6c42..4ed6afc 100644 --- a/src/teilchensammler_cli/models.py +++ b/src/teilchensammler_cli/models.py @@ -9,7 +9,6 @@ from sqlmodel import ( select, ) -from .database import engine logger = logging.getLogger(__name__) @@ -82,8 +81,7 @@ async def make_teilchen_input(text: str) -> TeilchenCreate | None: return teilchen -async def load_initial_data() -> Sequence[Teilchen]: - from .database import engine +async def load_initial_data(engine) -> Sequence[Teilchen]: with Session(engine) as session: statement = select( @@ -93,7 +91,7 @@ async def load_initial_data() -> Sequence[Teilchen]: return all_teilchen -async def add_to_database(tc: TeilchenCreate) -> Teilchen: +async def add_to_database(tc: TeilchenCreate, engine) -> Teilchen: with Session(engine) as session: teilchen = Teilchen.model_validate(tc) session.add(teilchen) diff --git a/tests.py b/tests.py index 4eb3280..844823e 100644 --- a/tests.py +++ b/tests.py @@ -1,6 +1,19 @@ -import pytest +from teilchensammler_cli.database import create_db_and_tables +from sqlalchemy.sql import text +import uuid +from typing import Generator -from teilchensammler_cli.models import TeilchenCreate, make_teilchen_input +import pytest +from sqlalchemy import Engine +from sqlmodel import Session, SQLModel, create_engine + +from teilchensammler_cli.models import ( + Teilchen, + TeilchenCreate, + add_to_database, + load_initial_data, + make_teilchen_input, +) @pytest.mark.final # don't run while we are fiddling with the app @@ -10,7 +23,7 @@ def test_initial_layout(snap_compare): assert snap_compare(app, terminal_size=(130, 40)) -empty_teilchen = { +empty_teilchen_data = { "name": "", "description": "", "tags": "", @@ -32,7 +45,7 @@ def TC(**kwargs) -> TeilchenCreate | None: - an instance of `TeilchenCreate` """ if kwargs: - arguments = empty_teilchen | kwargs + arguments = empty_teilchen_data | kwargs return TeilchenCreate(**arguments) # ty:ignore[invalid-argument-type] @@ -46,11 +59,15 @@ def TC(**kwargs) -> TeilchenCreate | None: (".a.", TC()), ("a", TC()), ("aa", TC()), + (" .", TC()), # still an empty string # Just enough for "name" ("a.", TC(name="a", text="a.")), ("aa.", TC(name="aa", text="aa.")), ("a..", TC(name="a", text="a..")), ("a.b.", TC(name="a", text="a.b.")), + ("1.", TC(name="1", text="1.")), # numbers can be names + ("_.", TC(name="_", text="_.")), # underscores can be names + ("a b.", TC(name="a b", text="a b.")), # names can contain spaces # Just enough for "name" and "description" ('a."b"', TC(name="a", description="b", text='a."b"')), ('a. "b"', TC(name="a", description="b", text='a. "b"')), @@ -68,6 +85,7 @@ def TC(**kwargs) -> TeilchenCreate | None: ("a. ##b", TC(name="a", description="", tags="#b", text="a. ##b")), ("a. #", TC(name="a", description="", tags="", text="a. #")), ("a. ##", TC(name="a", description="", tags="", text="a. ##")), + ("a. #tag with spaces", TC(name="a", tags="#tag", text="a. #tag with spaces")), # do we care about "number"? ], ) @@ -78,3 +96,96 @@ async def test_maketeilcheninput_can_create_desired_teilchen( actual = await make_teilchen_input(example_input) assert expected == actual + + +@pytest.fixture(name="engine") +def in_memory_db() -> Generator[Engine]: + """Creates an in-memory sqlite database + + Yields: + The engine used to create the database. + """ + + engine = create_engine("sqlite:///:memory:") + + yield engine + + engine.dispose() + + +@pytest.fixture(name="app_db") +def database_ready_to_use(engine: Engine) -> Generator[Engine]: + SQLModel.metadata.create_all(engine) + yield engine + + +@pytest.fixture(name="db_teilchen") +def teilchen_in_db(app_db: Engine) -> Generator[Teilchen]: + """Creates a new Teilchen and stores it in the database. + + Args: + engine: an instance of sqlalchemy.Engine which was used to create the database. + + Yields: + The Teilchen refreshed from the database. + """ + import uuid + + teilchen = Teilchen( + id=uuid.uuid7(), name="TT", description="Test Teilchen", number=1, tags="", text="" + ) + with Session(app_db) as session: + session.add(teilchen) + session.commit() + session.refresh(teilchen) + + yield teilchen + + with Session(app_db) as session: + session.delete(teilchen) + session.commit() + + +async def test_loadinitialdata_returns_expected_data(app_db: Engine, db_teilchen: Teilchen): + all_data = await load_initial_data(app_db) + assert len(all_data) == 1 + + fetched_data: tuple = all_data[0] + teilchen_data: dict[str, str | int] = { + "id": fetched_data[0], + "name": fetched_data[1], + "description": fetched_data[2], + "number": 1, + "tags": "", + "text": "", + } + + fetched_teilchen = Teilchen.model_validate(teilchen_data) + + assert fetched_teilchen == db_teilchen + + +async def test_data_provided_to_addtodatabase_ends_up_in_database(app_db: Engine): + all_data = await load_initial_data(app_db) + assert len(all_data) == 0 + + teilchen = Teilchen( + id=uuid.uuid7(), name="test", description="test", tags="#test", number=1, text="test" + ) + + db_teilchen = await add_to_database(teilchen, app_db) + + assert teilchen == db_teilchen + + +async def test_created_database_contains_expected_tables(engine: Engine): + + statement = text("SELECT name from sqlite_schema WHERE type = 'table'") + with engine.connect() as connection: + results = connection.execute(statement).all() + assert len(results) == 0 + + create_db_and_tables(engine) + + results = connection.execute(statement).all() + assert len(results) == 1 diff --git a/uv.lock b/uv.lock index 0c0c8bc..4c6979b 100644 --- a/uv.lock +++ b/uv.lock @@ -1153,7 +1153,7 @@ wheels = [ [[package]] name = "teilchensammler-cli" -version = "0.5.0" +version = "0.5.1" source = { editable = "." } dependencies = [ { name = "ciso8601" },