diff --git a/src/teilchensammler_cli/main.py b/src/teilchensammler_cli/main.py index f2f1e8f..8c789cb 100644 --- a/src/teilchensammler_cli/main.py +++ b/src/teilchensammler_cli/main.py @@ -2,103 +2,47 @@ This is where the application is implemented. """ -from typing import Literal, final, override +import logging -from textual.app import App, ComposeResult -from textual.containers import HorizontalGroup -from textual.screen import Screen -from textual.widget import Widget -from textual.widgets import ( - Button, - DataTable, - Footer, - Header, - Input, - Static, -) +from sqlmodel import Session +from textual.app import App + +from .database import create_db_and_tables, engine +from .models import Teilchen, make_teilchen_input +from .tui import AddInventoryScreen + +logger = logging.getLogger(__name__) -@final -class SearchBar(Static): - DEFAULT_CSS = """ - #teilchen-input { - width: 4fr; - } - #button-search, #button-add { - width: 1fr; - } - """ - - @override - def compose(self) -> ComposeResult: - with HorizontalGroup(id="search-bar-widget"): - yield Input( - placeholder="Enter Teilchen information: name, description, #tags", - tooltip=( - "This is a free-form field: Enter a name and " - "description any way you like. You should use #hashtags for any " - "meta information." - ), - id="teilchen-input", - type="text", - ) - yield Button("Add", variant="success", classes="search-bar-buttons", id="button-add") - yield Button( - "Search", - variant="default", - classes="search-bar-buttons", - id="button-search", - ) - - -TeilchenDatum = tuple[int, str, str, str, str] -TeilchenHeader = tuple[ - Literal["pk"], - Literal["Name"], - Literal["Description"], - Literal["Number"], - Literal["Tags"], -] - - -FAKE_DATA: list[TeilchenHeader | TeilchenDatum] = [ - ("pk", "Name", "Description", "Number", "Tags"), - (0, "Name0", "Description0", "9000", "#tag0 #tag00 #tag000"), - (1, "Name1", "Description1", "9001", "#tag1 #tag11 #tag111"), - (2, "Name2", "Description2", "9002", "#tag2 #tag22 #tag222"), - (3, "Name3", "Description3", "9003", "#tag3 #tag33 #tag333"), - (4, "Name4", "Description4", "9004", "#tag4 #tag44 #tag444"), - (5, "Name5", "Description5", "9005", "#tag5 #tag55 #tag555"), -] - - -@final -class SearchResults(Widget): - @override - def compose(self) -> ComposeResult: - yield DataTable(id="table-search-result", cursor_type="row", zebra_stripes=True) - - async def on_mount(self) -> None: - table: DataTable[None] = self.query_one(DataTable[None]) - _ = table.add_columns(*FAKE_DATA[0]) # pyright: ignore[reportArgumentType] - _ = table.add_rows(FAKE_DATA[1:]) # pyright: ignore[reportArgumentType] - - -class AddInventoryScreen(Screen[None]): - @override - def compose(self) -> ComposeResult: - yield Header() - yield SearchBar() - yield SearchResults() - yield Footer() - - -@final class SammlerApp(App[None]): async def on_mount(self) -> None: + create_db_and_tables() _ = self.push_screen(AddInventoryScreen()) +FAKE_INPUT = """ + Ein Teilchen. #Tag03 #tag12 "Dieses Teilchen ist nur zum testen" #tag1 #tag2 + """.strip(" \n") + + +def try_this(): + teilchen_data = make_teilchen_input(FAKE_INPUT) + if not teilchen_data: + logger.error("oh no!") + + with Session(engine) as session: + db_teilchen = Teilchen.model_validate(teilchen_data) + session.add(db_teilchen) + session.commit() + session.refresh(db_teilchen) + + print(f"{db_teilchen=}") + + def main() -> None: app = SammlerApp() app.run() + + +if __name__ == "__main__": + main() diff --git a/src/teilchensammler_cli/models.py b/src/teilchensammler_cli/models.py index 13e7dc2..35503c5 100644 --- a/src/teilchensammler_cli/models.py +++ b/src/teilchensammler_cli/models.py @@ -1,23 +1,57 @@ +import logging +import re import uuid +from natsort import natsorted from sqlmodel import ( Field, SQLModel, ) +logger = logging.getLogger(__name__) -class TeilchenInput(SQLModel): + +class TeilchenBase(SQLModel): text: str -class Teilchen(TeilchenInput, table=True): +class TeilchenCreate(TeilchenBase): + description: str | None + name: str = Field(index=True) + number: int = Field(default=1) + tags: str | None + + text: str # The original input as entered by the user + + +class Teilchen(TeilchenCreate, table=True): id: uuid.UUID = Field(default_factory=uuid.uuid7, primary_key=True) - name: str = Field(index=True) - description: str | None - tags: str | None - number: int = Field(default=1) +def make_teilchen_input(text: str) -> TeilchenCreate | None: + if not text: + logger.error("Empty text.") + return None + name_end = text.find(".") + name = text[0:name_end] + if not name: + logger.error("Could not extract name.") + return None + description_start = text.find('"', name_end + 1) + description_end = text.find('"', description_start + 1) + description = text[description_start:description_end] + if not description: + logger.warning("Could not extract description.") + tags = re.findall(r"#\w+", text.lower()) + if not tags: + logger.warning("No tags found in text") + + return TeilchenCreate( + name=name, + description=description, + tags=" ".join(natsorted(tags)), + text=text, + ) diff --git a/src/teilchensammler_cli/tui.py b/src/teilchensammler_cli/tui.py new file mode 100644 index 0000000..5f187bd --- /dev/null +++ b/src/teilchensammler_cli/tui.py @@ -0,0 +1,82 @@ +from typing import Literal, final, override + +from textual.app import ComposeResult +from textual.containers import HorizontalGroup +from textual.screen import Screen +from textual.widget import Widget +from textual.widgets import DataTable, Footer, Header, Static, Input, Button + + +@final +class SearchBar(Static): + DEFAULT_CSS = """ + #teilchen-input { + width: 4fr; + } + #button-search, #button-add { + width: 1fr; + } + """ + + @override + def compose(self) -> ComposeResult: + with HorizontalGroup(id="search-bar-widget"): + yield Input( + placeholder="Enter Teilchen information: name, description, #tags", + tooltip=( + "This is a free-form field: Enter a name and " + "description any way you like. You should use #hashtags for any " + "meta information." + ), + id="teilchen-input", + type="text", + ) + yield Button("Add", variant="success", classes="search-bar-buttons", id="button-add") + yield Button( + "Search", + variant="default", + classes="search-bar-buttons", + id="button-search", + ) + + +TeilchenDatum = tuple[int, str, str, str, str] +TeilchenHeader = tuple[ + Literal["pk"], + Literal["Name"], + Literal["Description"], + Literal["Number"], + Literal["Tags"], +] + + +FAKE_DATA: list[TeilchenHeader | TeilchenDatum] = [ + ("pk", "Name", "Description", "Number", "Tags"), + (0, "Name0", "Description0", "9000", "#tag0 #tag00 #tag000"), + (1, "Name1", "Description1", "9001", "#tag1 #tag11 #tag111"), + (2, "Name2", "Description2", "9002", "#tag2 #tag22 #tag222"), + (3, "Name3", "Description3", "9003", "#tag3 #tag33 #tag333"), + (4, "Name4", "Description4", "9004", "#tag4 #tag44 #tag444"), + (5, "Name5", "Description5", "9005", "#tag5 #tag55 #tag555"), +] + + +@final +class SearchResults(Widget): + @override + def compose(self) -> ComposeResult: + yield DataTable(id="table-search-result", cursor_type="row", zebra_stripes=True) + + async def on_mount(self) -> None: + table: DataTable[None] = self.query_one(DataTable[None]) + _ = table.add_columns(*FAKE_DATA[0]) # pyright: ignore[reportArgumentType] + _ = table.add_rows(FAKE_DATA[1:]) # pyright: ignore[reportArgumentType] + + +class AddInventoryScreen(Screen[None]): + @override + def compose(self) -> ComposeResult: + yield Header() + yield SearchBar() + yield SearchResults() + yield Footer()