Compare commits

..

7 commits

Author SHA1 Message Date
bronsen
278bd4a8ed release: bump version since we have more tests now
All checks were successful
ci/woodpecker/tag/workflow Pipeline was successful
ci/woodpecker/push/workflow Pipeline was successful
re: #14
2026-02-22 19:06:31 +01:00
bronsen
4832a0d69b codestyle: move "statement" out of with block 2026-02-22 19:05:37 +01:00
bronsen
1fdf5cb1d3 codestyle: remove unhelpful type hints 2026-02-22 19:04:46 +01:00
bronsen
aecd10115e tests: ensure helper function actually updates database schema 2026-02-22 18:58:05 +01:00
bronsen
aba919adca tests: separate "engine" from "db with tables" 2026-02-22 18:57:16 +01:00
bronsen
a4df79cda3 tests: ensure we actually commit given data to the database 2026-02-22 18:57:16 +01:00
bronsen
d205f4909e test: ensure we read expected data from database 2026-02-22 18:57:16 +01:00
6 changed files with 124 additions and 15 deletions

View file

@ -1,6 +1,6 @@
[project] [project]
name = "teilchensammler-cli" name = "teilchensammler-cli"
version = "0.5.0" version = "0.5.1"
description = "Build up and maintain an inventory of electronics parts and tools." description = "Build up and maintain an inventory of electronics parts and tools."
readme = "README.md" readme = "README.md"
requires-python = ">=3.14,<4.0" requires-python = ">=3.14,<4.0"

View file

@ -6,5 +6,5 @@ sqlite_url = os.environ.get("DATABASE_URL", "sqlite:///database.db")
engine = create_engine(sqlite_url, echo=False) engine = create_engine(sqlite_url, echo=False)
def create_db_and_tables(): def create_db_and_tables(engine):
SQLModel.metadata.create_all(engine) SQLModel.metadata.create_all(engine)

View file

@ -12,7 +12,7 @@ from textual.screen import Screen
from textual.widget import Widget from textual.widget import Widget
from textual.widgets import Button, DataTable, Footer, Header, Input, Static 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 from .models import add_to_database, load_initial_data, make_teilchen_input
logging.basicConfig( logging.basicConfig(
@ -26,7 +26,7 @@ TEILCHEN_DATA_HEADER = "pk Name Description Number Tags".split()
class SammlerApp(App): class SammlerApp(App):
async def on_mount(self) -> None: async def on_mount(self) -> None:
create_db_and_tables() create_db_and_tables(engine)
self.push_screen(AddInventoryScreen()) self.push_screen(AddInventoryScreen())
@ -68,7 +68,7 @@ class SearchBar(Static):
event.input.value = "" event.input.value = ""
teilchen = await add_to_database(tc) teilchen = await add_to_database(tc, engine)
self.screen.query_one(DataTable).add_row( self.screen.query_one(DataTable).add_row(
teilchen.id, teilchen.name, teilchen.description, teilchen.number, teilchen.tags teilchen.id, teilchen.name, teilchen.description, teilchen.number, teilchen.tags
) )
@ -81,7 +81,7 @@ class SearchResults(Widget):
async def on_mount(self) -> None: async def on_mount(self) -> None:
table: DataTable = self.query_one(DataTable) table: DataTable = self.query_one(DataTable)
table.add_columns(*TEILCHEN_DATA_HEADER) table.add_columns(*TEILCHEN_DATA_HEADER)
table.add_rows(await load_initial_data()) table.add_rows(await load_initial_data(engine))
class AddInventoryScreen(Screen): class AddInventoryScreen(Screen):

View file

@ -9,7 +9,6 @@ from sqlmodel import (
select, select,
) )
from .database import engine
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -82,8 +81,7 @@ async def make_teilchen_input(text: str) -> TeilchenCreate | None:
return teilchen return teilchen
async def load_initial_data() -> Sequence[Teilchen]: async def load_initial_data(engine) -> Sequence[Teilchen]:
from .database import engine
with Session(engine) as session: with Session(engine) as session:
statement = select( statement = select(
@ -93,7 +91,7 @@ async def load_initial_data() -> Sequence[Teilchen]:
return all_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: with Session(engine) as session:
teilchen = Teilchen.model_validate(tc) teilchen = Teilchen.model_validate(tc)
session.add(teilchen) session.add(teilchen)

119
tests.py
View file

@ -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 @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)) assert snap_compare(app, terminal_size=(130, 40))
empty_teilchen = { empty_teilchen_data = {
"name": "", "name": "",
"description": "", "description": "",
"tags": "", "tags": "",
@ -32,7 +45,7 @@ def TC(**kwargs) -> TeilchenCreate | None:
- an instance of `TeilchenCreate` - an instance of `TeilchenCreate`
""" """
if kwargs: if kwargs:
arguments = empty_teilchen | kwargs arguments = empty_teilchen_data | kwargs
return TeilchenCreate(**arguments) # ty:ignore[invalid-argument-type] return TeilchenCreate(**arguments) # ty:ignore[invalid-argument-type]
@ -46,11 +59,15 @@ def TC(**kwargs) -> TeilchenCreate | None:
(".a.", TC()), (".a.", TC()),
("a", TC()), ("a", TC()),
("aa", TC()), ("aa", TC()),
(" .", TC()), # still an empty string
# Just enough for "name" # Just enough for "name"
("a.", TC(name="a", text="a.")), ("a.", TC(name="a", text="a.")),
("aa.", TC(name="aa", text="aa.")), ("aa.", TC(name="aa", text="aa.")),
("a..", TC(name="a", text="a..")), ("a..", TC(name="a", text="a..")),
("a.b.", TC(name="a", text="a.b.")), ("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" # 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"')),
('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. ##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. ##", 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"? # do we care about "number"?
], ],
) )
@ -78,3 +96,96 @@ async def test_maketeilcheninput_can_create_desired_teilchen(
actual = await make_teilchen_input(example_input) actual = await make_teilchen_input(example_input)
assert expected == actual 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

2
uv.lock generated
View file

@ -1153,7 +1153,7 @@ wheels = [
[[package]] [[package]]
name = "teilchensammler-cli" name = "teilchensammler-cli"
version = "0.5.0" version = "0.5.1"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "ciso8601" }, { name = "ciso8601" },