[project] extract tui code into own module, also extraxt (db) models into their own module
Some checks failed
ci/woodpecker/push/workflow Pipeline failed
Some checks failed
ci/woodpecker/push/workflow Pipeline failed
This commit is contained in:
parent
d58ae7aad1
commit
173d23addf
3 changed files with 155 additions and 95 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
82
src/teilchensammler_cli/tui.py
Normal file
82
src/teilchensammler_cli/tui.py
Normal file
|
|
@ -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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue