Compare commits

...

5 Commits

Author SHA1 Message Date
jpt
92cc3c5b40 confirm dialog 2025-04-11 07:25:16 -05:00
jpt
8323323dea toml colors 2025-04-10 23:43:44 -05:00
jpt
77ac5a5cd2 moving forward on toml fields 2025-04-10 22:47:36 -05:00
jpt
0d6c20cff6 refactored fields; added date 2025-04-10 22:18:46 -05:00
jpt
9c55adb88e mostly working modal 2025-04-10 20:30:31 -05:00
13 changed files with 345 additions and 107 deletions

View File

@ -9,6 +9,7 @@ dependencies = [
"lxml>=5.3.0", "lxml>=5.3.0",
"peewee>=3.17.8", "peewee>=3.17.8",
"textual>=1.0.0", "textual>=1.0.0",
"tomlkit>=0.13.2",
"typer>=0.15.1", "typer>=0.15.1",
] ]

14
src/tt/config.py Normal file
View File

@ -0,0 +1,14 @@
import tomlkit
def get_enum(name):
with open("tt.toml", "r") as f:
config = tomlkit.load(f)
for enum in config.get("enums", []):
if enum["name"] == name:
return {v["value"]: v for v in enum["values"]}
raise ValueError(f"no such enum! {name}")
STATUSES = get_enum("status")
PROJECTS = get_enum("projects")

View File

@ -1,6 +1,6 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from peewee import fn, JOIN from peewee import fn, JOIN
from ..db import Task, Category, TaskStatus from ..db import Task, Category
def get_category_summary(num: int = 5) -> list[dict]: def get_category_summary(num: int = 5) -> list[dict]:
@ -20,7 +20,7 @@ def get_category_summary(num: int = 5) -> list[dict]:
overdue_count = ( overdue_count = (
Task.select(Task.category, fn.COUNT(Task.id).alias("overdue")) Task.select(Task.category, fn.COUNT(Task.id).alias("overdue"))
.where( .where(
(~Task.deleted) & (Task.due < now) & (Task.status != TaskStatus.DONE.value) (~Task.deleted) & (Task.due < now) & (Task.status != "done")
) )
.group_by(Task.category) .group_by(Task.category)
) )
@ -32,7 +32,7 @@ def get_category_summary(num: int = 5) -> list[dict]:
(~Task.deleted) (~Task.deleted)
& (Task.due >= now) & (Task.due >= now)
& (Task.due <= week_from_now) & (Task.due <= week_from_now)
& (Task.status != TaskStatus.DONE.value) & (Task.status != "done")
) )
.group_by(Task.category) .group_by(Task.category)
) )
@ -41,16 +41,16 @@ def get_category_summary(num: int = 5) -> list[dict]:
query = ( query = (
Category.select( Category.select(
Category.name, Category.name,
fn.COALESCE(fn.SUM(Task.status == TaskStatus.ZERO.value), 0).alias( fn.COALESCE(fn.SUM(Task.status == "zero"), 0).alias(
"zero_count" "zero_count"
), ),
fn.COALESCE(fn.SUM(Task.status == TaskStatus.WIP.value), 0).alias( fn.COALESCE(fn.SUM(Task.status == "wip"), 0).alias(
"wip_count" "wip_count"
), ),
fn.COALESCE(fn.SUM(Task.status == TaskStatus.BLOCKED.value), 0).alias( fn.COALESCE(fn.SUM(Task.status == "blocked"), 0).alias(
"blocked_count" "blocked_count"
), ),
fn.COALESCE(fn.SUM(Task.status == TaskStatus.DONE.value), 0).alias( fn.COALESCE(fn.SUM(Task.status == "done"), 0).alias(
"done_count" "done_count"
), ),
fn.COALESCE(overdue_count.c.overdue, 0).alias("overdue"), fn.COALESCE(overdue_count.c.overdue, 0).alias("overdue"),
@ -148,7 +148,7 @@ def get_due_soon(
(~Task.deleted) (~Task.deleted)
& (Task.due.is_null(False)) & (Task.due.is_null(False))
& (Task.due != "") & (Task.due != "")
& (Task.status != TaskStatus.DONE.value) & (Task.status != "done")
) )
) )

View File

@ -2,7 +2,8 @@ import json
from datetime import datetime from datetime import datetime
from peewee import fn from peewee import fn
from peewee import Case, Value from peewee import Case, Value
from ..db import db, Task, Category, TaskStatus, SavedSearch from ..db import db, Task, Category, SavedSearch
from .. import config
def category_lookup(category): def category_lookup(category):
@ -18,7 +19,7 @@ def get_task(item_id: int) -> Task:
def add_task( def add_task(
text: str, text: str,
category: str, category: str,
status: str = TaskStatus.ZERO.value, status: str,
due: datetime | None = None, due: datetime | None = None,
type: str = "", type: str = "",
) -> Task: ) -> Task:
@ -62,7 +63,7 @@ def _parse_sort_string(sort_string, status_order):
if field == "status": if field == "status":
if not status_order: if not status_order:
status_order = [s.value for s in TaskStatus] status_order = list(config.STATUSES.keys())
# CASE statement that maps each status to its position in the order # CASE statement that maps each status to its position in the order
order_case = Case( order_case = Case(
Task.status, Task.status,

View File

@ -20,15 +20,6 @@ db = SqliteDatabase(
}, },
) )
class TaskStatus(Enum):
# order is used for progression in toggle
ZERO = "zero"
WIP = "wip"
BLOCKED = "blocked"
DONE = "done"
class GeneratorType(Enum): class GeneratorType(Enum):
DAYS_BETWEEN = "days-btwn" DAYS_BETWEEN = "days-btwn"
MONTHLY = "monthly" MONTHLY = "monthly"
@ -48,10 +39,7 @@ class Category(BaseModel):
class Task(BaseModel): class Task(BaseModel):
text = TextField() text = TextField()
status = CharField( status = CharField()
choices=[(status.value, status.name) for status in TaskStatus],
default=TaskStatus.ZERO.value,
)
due = DateTimeField(null=True) due = DateTimeField(null=True)
category = ForeignKeyField(Category, backref="tasks", null=True) category = ForeignKeyField(Category, backref="tasks", null=True)
type = CharField() type = CharField()

View File

@ -1,6 +1,7 @@
import csv import csv
from datetime import datetime from datetime import datetime
from tt.db import initialize_db, Task, Category, TaskStatus from tt.db import initialize_db, Task, Category
from tt.config import STATUSES
def import_tasks_from_csv(filename: str): def import_tasks_from_csv(filename: str):
@ -20,9 +21,7 @@ def import_tasks_from_csv(filename: str):
# Validate status # Validate status
status = row["status"].lower() if row["status"] else "zero" status = row["status"].lower() if row["status"] else "zero"
try: if status not in STATUSES:
TaskStatus(status)
except ValueError:
print(f"Warning: Invalid status '{status}', defaulting to 'zero'") print(f"Warning: Invalid status '{status}', defaulting to 'zero'")
status = "zero" status = "zero"

View File

@ -1,3 +1,4 @@
import datetime
from textual.app import App from textual.app import App
from textual.widgets import ( from textual.widgets import (
DataTable, DataTable,
@ -10,30 +11,15 @@ from textual.containers import Container
from ..utils import ( from ..utils import (
remove_rich_tag, remove_rich_tag,
filter_to_string, filter_to_string,
advance_enum_val,
get_text_from_editor, get_text_from_editor,
) )
from .keymodal import KeyModal from .keymodal import KeyModal
from .modals import ChoiceModal, DateModal, ConfirmModal
ELLIPSIS = ""
class NotifyValidationError(Exception): class NotifyValidationError(Exception):
"""will notify and continue if raised""" """will notify and continue if raised"""
def _enum_preprocessor(enumCls):
"""generate a default preprocessor to enforce enums"""
def preprocessor(val):
try:
enumCls(val)
return val
except ValueError:
raise NotifyValidationError(
f"Invalid value. Use: {[s.value for s in enumCls]}"
)
class TableColumnConfig: class TableColumnConfig:
def __init__( def __init__(
@ -42,8 +28,6 @@ class TableColumnConfig:
display_name: str, display_name: str,
*, *,
default=None, default=None,
enum=None,
preprocessor=None,
enable_editor=False, enable_editor=False,
filterable=True, filterable=True,
read_only=False, read_only=False,
@ -51,16 +35,57 @@ class TableColumnConfig:
self.field = field self.field = field
self.display_name = display_name self.display_name = display_name
self.default = default self.default = default
self.enum = enum
self.enable_editor = enable_editor self.enable_editor = enable_editor
self.filterable = filterable self.filterable = filterable
self.read_only = read_only self.read_only = read_only
if preprocessor:
self.preprocessor = preprocessor def preprocess(self, val):
elif self.enum: return val # no-op
self.preprocessor = _enum_preprocessor(self.enum)
def start_change(self, app, current_value):
if current_value.endswith(ELLIPSIS):
app.action_start_edit()
else: else:
self.preprocessor = lambda x: x # default edit mode
app._show_input("edit", current_value)
class EnumColumnConfig(TableColumnConfig):
def __init__(
self,
field: str,
display_name: str,
enum,
**kwargs
):
super().__init__(field, display_name, **kwargs)
self.enum = enum
def preprocess(self, val):
if val in self.enum:
return val
else:
raise NotifyValidationError(
f"Invalid value {val}. Use: {list(self.enum)}"
)
def start_change(self, app, current_value):
# a weird hack? pass app here and correct modal gets pushed
app.push_screen(ChoiceModal(self.enum, current_value), app.apply_change)
class DateColumnConfig(TableColumnConfig):
def preprocess(self, val):
try:
return datetime.strptime(val, "%Y-%m-%d")
except ValueError:
raise NotifyValidationError("Invalid date format. Use YYYY-MM-DD")
def start_change(self, app, current_value):
app.push_screen(DateModal(current_value), app.apply_change)
ELLIPSIS = ""
class TableEditor(App): class TableEditor(App):
@ -134,7 +159,7 @@ class TableEditor(App):
("?", "show_keys", "show keybindings"), ("?", "show_keys", "show keybindings"),
] ]
def __init__(self, default_view="default"): def __init__(self):
super().__init__() super().__init__()
self.filters = {} self.filters = {}
self.sort_string = "" # TODO: default sort self.sort_string = "" # TODO: default sort
@ -212,7 +237,10 @@ class TableEditor(App):
self.table.move_cursor(row=0, column=1) self.table.move_cursor(row=0, column=1)
def action_delete_item(self): def action_delete_item(self):
if self.table.cursor_column == 0: self.push_screen(ConfirmModal(f"delete ?"), self._delete_item_callback)
def _delete_item_callback(self, confirm):
if confirm and self.table.cursor_column == 0:
cur_row = self.table.cursor_row cur_row = self.table.cursor_row
item_id = int(self.table.get_cell_at((cur_row, 0))) item_id = int(self.table.get_cell_at((cur_row, 0)))
# deletable items need a delete # deletable items need a delete
@ -229,10 +257,8 @@ class TableEditor(App):
continue continue
val = self.filters.get(fc.field, fc.default) val = self.filters.get(fc.field, fc.default)
if val is not None: if val is not None:
# enums use comma separated filters if "," in val:
if fc.enum: val = val.split(",")[0] # TODO: fix hack for enums
prepopulated[fc.field] = val.split(",")[0]
else:
prepopulated[fc.field] = val prepopulated[fc.field] = val
new_item = self.add_item_callback(**prepopulated) new_item = self.add_item_callback(**prepopulated)
@ -247,21 +273,21 @@ class TableEditor(App):
def _active_item_id(self): def _active_item_id(self):
return int(self.table.get_cell_at((self.table.cursor_row, 0))) return int(self.table.get_cell_at((self.table.cursor_row, 0)))
def action_toggle_cell(self): # def action_toggle_cell(self):
cur_row = self.table.cursor_row # cur_row = self.table.cursor_row
cur_col = self.table.cursor_column # cur_col = self.table.cursor_column
cconf = self._active_column_config() # cconf = self._active_column_config()
if cconf.enum: # if cconf.enum:
item_id = self._active_item_id() # item_id = self._active_item_id()
current_val = self.table.get_cell_at((cur_row, cur_col)) # current_val = self.table.get_cell_at((cur_row, cur_col))
next_val = advance_enum_val(cconf.enum, current_val) # next_val = advance_enum_val(cconf.enum, current_val)
self.table.update_cell_at((cur_row, cur_col), next_val) # self.table.update_cell_at((cur_row, cur_col), next_val)
# trigger item_id to be saved on the next cursor move # # trigger item_id to be saved on the next cursor move
# this avoids filtered columns disappearing right away # # this avoids filtered columns disappearing right away
# and tons of DB writes # # and tons of DB writes
update = {cconf.field: next_val} # update = {cconf.field: next_val}
self._register_save_on_move(item_id, **update) # self._register_save_on_move(item_id, **update)
def _register_save_on_move(self, item_id, **kwargs): def _register_save_on_move(self, item_id, **kwargs):
if self.save_on_move and self.save_on_move["item_id"] != item_id: if self.save_on_move and self.save_on_move["item_id"] != item_id:
@ -333,15 +359,15 @@ class TableEditor(App):
if cconf.read_only: if cconf.read_only:
return return
# save cursor before callback, so correct position updates
self._save_cursor() self._save_cursor()
current_value = self.table.get_cell_at( current_value = self.table.get_cell_at(
(self.table.cursor_row, self.table.cursor_column) (self.table.cursor_row, self.table.cursor_column)
) )
if current_value.endswith(ELLIPSIS):
self.notify("multi-line text, use (e)dit")
return
current_value = remove_rich_tag(current_value) current_value = remove_rich_tag(current_value)
self._show_input("edit", current_value)
# delegate to start_change, which will call back to self
cconf.start_change(self, current_value)
def action_start_edit(self): def action_start_edit(self):
cconf = self._active_column_config() cconf = self._active_column_config()
@ -392,7 +418,7 @@ class TableEditor(App):
# preprocess/validate the field being saved # preprocess/validate the field being saved
try: try:
update_data[field] = cconf.preprocessor(new_value) update_data[field] = cconf.preprocess(new_value)
self.update_item_callback(item_id, **update_data) self.update_item_callback(item_id, **update_data)
self.refresh_data() self.refresh_data()
except NotifyValidationError as e: except NotifyValidationError as e:

186
src/tt/tui/modals.py Normal file
View File

@ -0,0 +1,186 @@
from textual.screen import ModalScreen
from textual.binding import Binding
from textual.widgets import RadioSet, RadioButton, Label
from .. import config
from ..utils import get_color_enum
class ConfirmModal(ModalScreen):
CSS = """
ConfirmModal {
align: center middle;
background: $primary 30%;
}
"""
BINDINGS = [
("y", "confirm", "Down"),
("n,escape", "cancel", "cancel"),
]
def __init__(self, message):
self.message = message
super().__init__()
def compose(self):
yield Label(self.message)
yield Label("(y)es")
yield Label("(n)o")
def action_confirm(self):
self.dismiss(True)
def action_cancel(self):
self.app.pop_screen()
class ChoiceModal(ModalScreen):
CSS = """
ChoiceModal {
align: center middle;
background: $primary 30%;
}
ChoiceModal Label {
height: 1;
}
"""
BINDINGS = [
("j", "cursor_down", "Down"),
("k", "cursor_up", "Up"),
Binding("enter", "select", "Select", priority=True),
("c", "select", "Select"),
("escape", "cancel", "cancel"),
]
def __init__(self, enum, selected):
self._enum = enum
self.selected = selected
super().__init__()
def compose(self):
yield RadioSet(
*[
RadioButton(
get_color_enum(e.value, config.STATUSES, "red"), value=self.selected == str(e.value)
)
for e in self._enum
]
)
def action_cursor_down(self):
self.query_one(RadioSet).action_next_button()
def action_cursor_up(self):
self.query_one(RadioSet).action_previous_button()
def action_select(self):
rs = self.query_one(RadioSet)
rs.action_toggle_button()
pressed = rs.pressed_button
self.dismiss(str(pressed.label))
def action_cancel(self):
self.app.pop_screen()
class DateModal(ModalScreen):
CSS = """
DateModal {
layout: horizontal;
align: center middle;
background: $primary 30%;
}
DateModal Label {
border: solid grey;
}
DateModal Label.selected-date {
border: solid green;
}
"""
BINDINGS = [
("j", "cursor_down", "Down"),
("k", "cursor_up", "Up"),
("h", "cursor_left", "Left"),
("l", "cursor_right", "Right"),
# ("0,1,2,3,4,5,6,7,8,9", "num_entry", "#"),
Binding("enter", "select", "Select", priority=True),
("escape", "cancel", "cancel"),
]
def __init__(self, date):
self.pieces = [int(p) for p in date.split("-")]
self.selected = 1 # start on month
super().__init__()
def compose(self):
for idx, piece in enumerate(self.pieces):
yield Label(
str(piece), classes="selected-date" if idx == self.selected else ""
)
def action_cursor_left(self):
# cycle Y/M/D
self.selected = (self.selected - 1) % 3
self._update_highlight()
def action_cursor_right(self):
self.selected = (self.selected + 1) % 3
self._update_highlight()
def _update_highlight(self):
for idx, lbl in enumerate(self.query("Label")):
if idx == self.selected:
lbl.add_class("selected-date")
else:
lbl.remove_class("selected-date")
def max_for(self, piece):
days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
if piece == 0:
return 3000
elif piece == 1:
return 12
else:
# -1 for offset array
return days_in_month[self.pieces[1] - 1]
def _move_piece(self, by):
cur_value = self.pieces[self.selected]
cur_value += by
if cur_value == 0:
cur_value = self.max_for(self.selected)
if cur_value > self.max_for(self.selected):
cur_value = 1
self.pieces[self.selected] = cur_value
cur_label = self.query("Label")[self.selected]
cur_label.update(str(cur_value))
def action_cursor_down(self):
self._move_piece(-1)
def action_cursor_up(self):
self._move_piece(1)
def on_key(self, event) -> None:
key = event.key
if key in "0123456789":
cur_value = self.pieces[self.selected]
# If we can append the values like 1+2 = 12
# then do that, otherwise reset the value
# so (for months) 1+3 = 3
appended = int(str(cur_value) + key)
if appended <= self.max_for(self.selected):
cur_value = appended
else:
cur_value = int(key)
self.pieces[self.selected] = cur_value
self._move_piece(0)
event.prevent_default()
def action_select(self):
date = "-".join(str(p) for p in self.pieces)
self.dismiss(date)
def action_cancel(self):
self.app.pop_screen()

View File

@ -11,6 +11,7 @@ from ..db import GeneratorType
from .editor import ( from .editor import (
TableEditor, TableEditor,
TableColumnConfig, TableColumnConfig,
EnumColumnConfig,
) )
@ -18,14 +19,14 @@ class TaskGenEditor(TableEditor):
TABLE_CONFIG = ( TABLE_CONFIG = (
TableColumnConfig("id", "ID"), TableColumnConfig("id", "ID"),
TableColumnConfig("template", "Template", default="recur {val}"), TableColumnConfig("template", "Template", default="recur {val}"),
TableColumnConfig( EnumColumnConfig(
"type", "type",
"Type", "Type",
default=GeneratorType.DAYS_BETWEEN.value, default=GeneratorType.DAYS_BETWEEN.value,
enum=GeneratorType, enum=GeneratorType,
), ),
TableColumnConfig("val", "Value", default="1"), TableColumnConfig("val", "Value", default="1"),
TableColumnConfig("next_at", "Next @", default="", read_only=True), TableColumnConfig("next_at", "Next @", read_only=True),
) )
def __init__(self): def __init__(self):

View File

@ -2,47 +2,40 @@ import json
from textual.widgets import Input from textual.widgets import Input
from datetime import datetime from datetime import datetime
from .. import config
from ..controller.tasks import ( from ..controller.tasks import (
get_task, get_task,
get_tasks, get_tasks,
add_task, add_task,
update_task, update_task,
TaskStatus,
save_view, save_view,
get_saved_view, get_saved_view,
) )
from ..utils import ( from ..utils import (
get_colored_category, get_color_enum,
get_colored_status,
get_colored_date, get_colored_date,
) )
from .editor import ( from .editor import (
TableEditor, TableEditor,
TableColumnConfig, TableColumnConfig,
NotifyValidationError, EnumColumnConfig,
DateColumnConfig,
ELLIPSIS, ELLIPSIS,
) )
def due_preprocessor(val):
try:
return datetime.strptime(val, "%Y-%m-%d")
except ValueError:
raise NotifyValidationError("Invalid date format. Use YYYY-MM-DD")
class TT(TableEditor): class TT(TableEditor):
TABLE_CONFIG = ( TABLE_CONFIG = (
TableColumnConfig("id", "ID"), TableColumnConfig("id", "ID"),
TableColumnConfig("text", "Task", default="new task", enable_editor=True), TableColumnConfig("text", "Task", default="new task", enable_editor=True),
TableColumnConfig( EnumColumnConfig(
"status", "status",
"Status", "Status",
enum=config.STATUSES,
default="zero", default="zero",
enum=TaskStatus,
), ),
TableColumnConfig("type", "Type", default=""), TableColumnConfig("type", "Type", default=""),
TableColumnConfig("due", "Due", default="", preprocessor=due_preprocessor), DateColumnConfig("due", "Due", default=""),
TableColumnConfig("category", "Category", default="main"), TableColumnConfig("category", "Category", default="main"),
) )
@ -96,10 +89,12 @@ class TT(TableEditor):
sort=self.sort_string, sort=self.sort_string,
) )
for item in items: for item in items:
category = get_colored_category( category = get_color_enum(
item.category.name if item.category else " - " item.category.name if item.category else " - ",
config.PROJECTS,
"grey"
) )
status = get_colored_status(item.status) status = get_color_enum(item.status, config.STATUSES, "red")
due = get_colored_date(item.due) due = get_colored_date(item.due)
if "\n" in item.text: if "\n" in item.text:

View File

@ -32,15 +32,9 @@ def advance_enum_val(enum_type, cur_val):
return members[next_idx] return members[next_idx]
def get_colored_status(status: str) -> str: def get_color_enum(value: str, enum: dict[str, dict], default: str) -> str:
colors = { color = enum.get(value, {"color": default})["color"]
"zero": "#666666", return f"[{color}]{value}[/]"
"wip": "#33aa99",
"blocked": "#cc9900",
"done": "#009900",
}
color = colors.get(status, "#666666")
return f"[{color}]{status}[/]"
def get_colored_category(category: str) -> str: def get_colored_category(category: str) -> str:

21
tt.toml Normal file
View File

@ -0,0 +1,21 @@
[[enums]]
name = "status"
values = [
{ value = "zero", color = "#666666" },
{ value = "blocked", color = "#33a99" },
{ value = "wip", color = "#cc9900" },
{ value = "done", color = "#009900" },
]
[[enums]]
name = "projects"
values = [
{ value = "SECT", color = "purple" },
{ value = "life", color = "#00cc00" },
{ value = "CAPP", color = "#cc0000" },
{ value = "ilikethis", color = "#cccc00" },
{ value = "krang", color = "#ff00ff"},
{ value = "artworld", color = "#0000cc"},
{ value = "TT", color = "#00ff00"},
]

14
uv.lock generated
View File

@ -1,4 +1,5 @@
version = 1 version = 1
revision = 1
requires-python = ">=3.10" requires-python = ">=3.10"
[[package]] [[package]]
@ -160,7 +161,7 @@ name = "click"
version = "8.1.8" version = "8.1.8"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "colorama", marker = "platform_system == 'Windows'" }, { name = "colorama", marker = "sys_platform == 'win32'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
wheels = [ wheels = [
@ -901,6 +902,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 },
] ]
[[package]]
name = "tomlkit"
version = "0.13.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955 },
]
[[package]] [[package]]
name = "tt" name = "tt"
version = "0.1.0" version = "0.1.0"
@ -910,6 +920,7 @@ dependencies = [
{ name = "lxml" }, { name = "lxml" },
{ name = "peewee" }, { name = "peewee" },
{ name = "textual" }, { name = "textual" },
{ name = "tomlkit" },
{ name = "typer" }, { name = "typer" },
] ]
@ -926,6 +937,7 @@ requires-dist = [
{ name = "lxml", specifier = ">=5.3.0" }, { name = "lxml", specifier = ">=5.3.0" },
{ name = "peewee", specifier = ">=3.17.8" }, { name = "peewee", specifier = ">=3.17.8" },
{ name = "textual", specifier = ">=1.0.0" }, { name = "textual", specifier = ">=1.0.0" },
{ name = "tomlkit", specifier = ">=0.13.2" },
{ name = "typer", specifier = ">=0.15.1" }, { name = "typer", specifier = ">=0.15.1" },
] ]