Compare commits

..

18 commits

Author SHA1 Message Date
bronsen
cf192c7d02 [deps] update dependencies 2026-02-07 02:54:10 +01:00
bronsen
e45dcaa0fd [deps] add ipython to dev dependencies
so we have available in our virtual env
2026-02-07 02:53:11 +01:00
bronsen
a45576406e [codestyle] clean up imports 2026-02-07 02:52:31 +01:00
bronsen
6a4334263a [models] move data loader to models module 2026-02-07 02:52:13 +01:00
bronsen
d2b6e11fe4 [typing] remove various decorators that we only used to hep basedpyright
ty solves this so much better
2026-02-07 02:51:02 +01:00
bronsen
312e60fa1f [models] move typedefs into models module 2026-02-07 02:49:38 +01:00
bronsen
a7d0bee0e7 add hints for ty, remove hints for basedpyright 2026-02-07 02:48:27 +01:00
bronsen
6155b54083 [models] make helper function async 2026-02-07 02:47:36 +01:00
bronsen
f4caba78e8 [models] don't warn on empty description 2026-02-07 02:46:44 +01:00
bronsen
50db5b9192 move special imports into helper function 2026-02-07 02:46:01 +01:00
bronsen
e438015ba8 [codestyle] move class definition closer to usage 2026-02-07 02:44:59 +01:00
bronsen
ddbfec4e54 [test] Make the tests pass even when we use relative imports 2026-01-08 12:08:13 +01:00
bronsen
8c0eff5693 [tooling] shut up basedpyright in this one case 2026-01-08 12:07:51 +01:00
bronsen
3e1bae138a [logging] End sentence of error message with a period "." 2026-01-08 12:07:31 +01:00
bronsen
a49cfd0dc1 [codestyle] add TODO item 2026-01-08 12:06:32 +01:00
bronsen
f622a58214 [models] simplify model hierarchy by removing one intermediate layer 2026-01-08 12:06:12 +01:00
bronsen
4236f39515 [mise] allow mise to create venv with specific python version 2026-01-08 12:05:22 +01:00
bronsen
13121637cf [codestyle] remove commented-out line of code 2026-01-08 12:04:36 +01:00
8 changed files with 578 additions and 405 deletions

7
mise.toml Normal file
View file

@ -0,0 +1,7 @@
[env]
'_'.python.venv = { path = ".venv", create = true }
[tools]
python = "3.14"
ruff = "latest"
uv = "latest"

View file

@ -49,6 +49,7 @@ teilchensammler-cli = "teilchensammler_cli.main:main"
[dependency-groups] [dependency-groups]
dev = [ dev = [
"ipython>=9.10.0",
"pytest>=9.0.1", "pytest>=9.0.1",
"pytest-asyncio>=1.3.0", "pytest-asyncio>=1.3.0",
"pytest-cov>=7.0.0", "pytest-cov>=7.0.0",

View file

@ -1,6 +1,6 @@
from sqlmodel import SQLModel, create_engine from sqlmodel import SQLModel, create_engine
sqlite_url = "sqlite:///:memory:" sqlite_url = "sqlite:///:memory:" # TODO: get url from environment
engine = create_engine(sqlite_url, echo=True) engine = create_engine(sqlite_url, echo=True)

View file

@ -19,13 +19,6 @@ logging.basicConfig(
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class SammlerApp(App[None]):
async def on_mount(self) -> None:
create_db_and_tables()
try_this()
_ = self.push_screen(AddInventoryScreen())
FAKE_INPUT = """ FAKE_INPUT = """
Ein Teilchen. #Tag03 #tag12 "Dieses Teilchen ist nur zum testen" #tag1 #tag2 Ein Teilchen. #Tag03 #tag12 "Dieses Teilchen ist nur zum testen" #tag1 #tag2
""".strip(" \n") """.strip(" \n")
@ -45,11 +38,17 @@ def try_this():
logger.info(f"{db_teilchen=}") logger.info(f"{db_teilchen=}")
class SammlerApp(App[None]):
async def on_mount(self) -> None:
create_db_and_tables()
try_this()
_ = self.push_screen(AddInventoryScreen())
app = SammlerApp() app = SammlerApp()
def main() -> None: def main() -> None:
# app = SammlerApp()
app.run() app.run()

View file

@ -1,8 +1,6 @@
import logging import logging
import re
import uuid import uuid
from natsort import natsorted
from sqlmodel import ( from sqlmodel import (
Field, Field,
SQLModel, SQLModel,
@ -11,24 +9,25 @@ from sqlmodel import (
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class TeilchenBase(SQLModel): TeilchenDatum = tuple[int, str, str, str, str]
text: str
class TeilchenCreate(TeilchenBase): class TeilchenCreate(SQLModel):
description: str | None description: str | None
name: str = Field(index=True) name: str = Field(index=True)
number: int = Field(default=1) number: int = Field(default=1)
text: str
tags: str | None tags: str | None
text: str # The original input as entered by the user
class Teilchen(TeilchenCreate, table=True): class Teilchen(TeilchenCreate, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid7, primary_key=True) id: uuid.UUID = Field(default_factory=uuid.uuid7, primary_key=True) # ty:ignore[unresolved-attribute]
def make_teilchen_input(text: str) -> TeilchenCreate | None: async def make_teilchen_input(text: str) -> TeilchenCreate | None:
import re
from natsort import natsorted
if not text: if not text:
logger.error("Empty text.") logger.error("Empty text.")
return None return None
@ -41,13 +40,14 @@ def make_teilchen_input(text: str) -> TeilchenCreate | None:
description_start = text.find('"', name_end + 1) description_start = text.find('"', name_end + 1)
description_end = text.find('"', description_start + 1) description_end = text.find('"', description_start + 1)
description = text[description_start:description_end] if description_end > description_start:
if not description: description = text[description_start:description_end]
logger.warning("Could not extract description.") else:
description = ""
tags = re.findall(r"#\w+", text.lower()) tags = re.findall(r"#\w+", text.lower())
if not tags: if not tags:
logger.warning("No tags found in text") logger.info("No tags found in text.")
return TeilchenCreate( return TeilchenCreate(
name=name, name=name,
@ -55,3 +55,14 @@ def make_teilchen_input(text: str) -> TeilchenCreate | None:
tags=" ".join(natsorted(tags)), tags=" ".join(natsorted(tags)),
text=text, text=text,
) )
async def load_initial_data() -> list[TeilchenDatum]:
return [
(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"),
]

View file

@ -1,13 +1,15 @@
from typing import Literal, final, override
from textual.app import ComposeResult from textual.app import ComposeResult
from textual.containers import HorizontalGroup from textual.containers import HorizontalGroup
from textual.screen import Screen from textual.screen import Screen
from textual.widget import Widget from textual.widget import Widget
from textual.widgets import DataTable, Footer, Header, Static, Input, Button from textual.widgets import Button, DataTable, Footer, Header, Input, Static
from .models import load_initial_data
FAKE_DATA_HEADER = "pk Name Description Number Tags"
@final
class SearchBar(Static): class SearchBar(Static):
DEFAULT_CSS = """ DEFAULT_CSS = """
#teilchen-input { #teilchen-input {
@ -18,7 +20,6 @@ class SearchBar(Static):
} }
""" """
@override
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
with HorizontalGroup(id="search-bar-widget"): with HorizontalGroup(id="search-bar-widget"):
yield Input( yield Input(
@ -40,41 +41,17 @@ class SearchBar(Static):
) )
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): class SearchResults(Widget):
@override
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield DataTable(id="table-search-result", cursor_type="row", zebra_stripes=True) yield DataTable(id="table-search-result", cursor_type="row", zebra_stripes=True)
async def on_mount(self) -> None: async def on_mount(self) -> None:
table: DataTable[None] = self.query_one(DataTable[None]) table: DataTable[None] = self.query_one(DataTable[None])
_ = table.add_columns(*FAKE_DATA[0]) # pyright: ignore[reportArgumentType] _ = table.add_columns(FAKE_DATA_HEADER)
_ = table.add_rows(FAKE_DATA[1:]) # pyright: ignore[reportArgumentType] _ = table.add_rows(await load_initial_data()) # ty:ignore[invalid-argument-type]
class AddInventoryScreen(Screen[None]): class AddInventoryScreen(Screen[None]):
@override
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Header() yield Header()
yield SearchBar() yield SearchBar()

View file

@ -1,2 +1,4 @@
def test_initial_layout(snap_compare): # pyright: ignore[reportMissingParameterType, reportUnknownParameterType] def test_initial_layout(snap_compare): # pyright: ignore[reportMissingParameterType, reportUnknownParameterType]
assert snap_compare("src/teilchensammler_cli/main.py", terminal_size=(130, 40)) from teilchensammler_cli.main import app
assert snap_compare(app, terminal_size=(130, 40))

878
uv.lock generated

File diff suppressed because it is too large Load diff