Skip to content

Commit e06ed32

Browse files
committed
New column types + example columns
Now supporting: - Many enums - date - datetime - bool
1 parent af02a07 commit e06ed32

File tree

5 files changed

+450
-2
lines changed

5 files changed

+450
-2
lines changed

src/appui/formatting.py

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,32 @@
22

33
from __future__ import annotations
44

5-
from typing import Final
5+
from typing import TYPE_CHECKING, Final
6+
7+
if TYPE_CHECKING:
8+
from datetime import date, datetime
9+
from enum import Enum
610

711
_NO_VALUE: Final[str] = "N/A"
12+
_DEFAULT_DATE_FORMAT: Final[str] = "%Y-%m-%d"
13+
_DEFAULT_DATETIME_FORMAT: Final[str] = "%Y-%m-%d %H:%M"
14+
_CHECKED_VALUE: Final[str] = "☑"
15+
_UNCHECKED_VALUE: Final[str] = "☐"
16+
17+
# TODO: Allow user-configurable date/time formats via the app configuration.
18+
19+
20+
def _as_title_case(value: str) -> str:
21+
"""Return a title-cased string with underscores treated as word separators.
22+
23+
Args:
24+
value (str): The string to normalize.
25+
26+
Returns:
27+
str: The normalized, title-cased label.
28+
"""
29+
30+
return " ".join(word.capitalize() for word in value.replace("_", " ").split())
831

932

1033
def as_percent(value: float | None) -> str:
@@ -72,3 +95,66 @@ def as_compact(value: int | None) -> str:
7295
return f"{value / 1000000000:.2f}B"
7396

7497
return f"{value / 1000000000000:.2f}T"
98+
99+
100+
def as_date(value: date | None, fmt: str | None = None) -> str:
101+
"""Return the value formatted as a date string.
102+
103+
Args:
104+
value (date | None): The date value to format.
105+
fmt (str | None): Optional format string to override the default.
106+
107+
Returns:
108+
str: The formatted date string or a placeholder if the value is None.
109+
"""
110+
111+
if value is None:
112+
return _NO_VALUE
113+
return value.strftime(fmt or _DEFAULT_DATE_FORMAT)
114+
115+
116+
def as_datetime(value: datetime | None, fmt: str | None = None) -> str:
117+
"""Return the value formatted as a datetime string.
118+
119+
Args:
120+
value (datetime | None): The datetime value to format.
121+
fmt (str | None): Optional format string to override the default.
122+
123+
Returns:
124+
str: The formatted datetime string or a placeholder if the value is None.
125+
"""
126+
127+
if value is None:
128+
return _NO_VALUE
129+
return value.strftime(fmt or _DEFAULT_DATETIME_FORMAT)
130+
131+
132+
def as_enum(value: Enum | None) -> str:
133+
"""Return the value formatted as a title-cased enum label.
134+
135+
Args:
136+
value (Enum | None): The enumeration value to format.
137+
138+
Returns:
139+
str: The formatted enum label or a placeholder if the value is None.
140+
"""
141+
142+
if value is None:
143+
return _NO_VALUE
144+
raw_value = value.value if isinstance(value.value, str) else value.name
145+
return _as_title_case(str(raw_value))
146+
147+
148+
def as_bool(*, value: bool | None) -> str:
149+
"""Return the value formatted as a checkbox string.
150+
151+
Args:
152+
value (bool | None): The boolean value to format.
153+
154+
Returns:
155+
str: A checked or unchecked box, or a placeholder if the value is None.
156+
"""
157+
158+
if value is None:
159+
return _NO_VALUE
160+
return _CHECKED_VALUE if value else _UNCHECKED_VALUE

src/appui/quote_column_definitions.py

Lines changed: 217 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,21 @@
66

77
from .enhanced_data_table import EnhancedTableCell
88
from .enums import Justify
9-
from .formatting import as_compact, as_float, as_percent
9+
from .formatting import (
10+
as_bool,
11+
as_compact,
12+
as_date,
13+
as_datetime,
14+
as_enum,
15+
as_float,
16+
as_percent,
17+
)
1018
from .quote_table import quote_column
1119

1220
if TYPE_CHECKING:
21+
from datetime import date, datetime
22+
from enum import Enum
23+
1324
from .quote_table import QuoteColumn
1425

1526

@@ -184,6 +195,127 @@ def __init__(
184195
)
185196

186197

198+
class DateCell(EnhancedTableCell):
199+
"""Cell that renders date values."""
200+
201+
def __init__(
202+
self,
203+
value: date | None,
204+
*,
205+
date_format: str | None = None,
206+
justification: Justify = Justify.LEFT,
207+
style: str = "",
208+
secondary_key: str | None = None,
209+
) -> None:
210+
"""Initialize the date cell.
211+
212+
Args:
213+
value (date | None): The date value to display.
214+
date_format (str | None): Optional format override for display.
215+
justification (Justify): The text justification.
216+
style (str): The style string for the cell.
217+
secondary_key (str | None): An optional secondary string key to use for
218+
tie-breaking during sorting.
219+
"""
220+
221+
safe_value = float("-inf") if value is None else value.toordinal()
222+
super().__init__(
223+
_with_secondary_key(safe_value, secondary_key),
224+
as_date(value, date_format),
225+
justification,
226+
style,
227+
)
228+
229+
230+
class DateTimeCell(EnhancedTableCell):
231+
"""Cell that renders datetime values."""
232+
233+
def __init__(
234+
self,
235+
value: datetime | None,
236+
*,
237+
datetime_format: str | None = None,
238+
justification: Justify = Justify.LEFT,
239+
style: str = "",
240+
secondary_key: str | None = None,
241+
) -> None:
242+
"""Initialize the datetime cell.
243+
244+
Args:
245+
value (datetime | None): The datetime value to display.
246+
datetime_format (str | None): Optional format override for display.
247+
justification (Justify): The text justification.
248+
style (str): The style string for the cell.
249+
secondary_key (str | None): An optional secondary string key to use for
250+
tie-breaking during sorting.
251+
"""
252+
253+
safe_value = float("-inf") if value is None else value.timestamp()
254+
super().__init__(
255+
_with_secondary_key(safe_value, secondary_key),
256+
as_datetime(value, datetime_format),
257+
justification,
258+
style,
259+
)
260+
261+
262+
class EnumCell(EnhancedTableCell):
263+
"""Cell that renders enum values in title case."""
264+
265+
def __init__(
266+
self,
267+
value: Enum | None,
268+
*,
269+
justification: Justify = Justify.LEFT,
270+
style: str = "",
271+
secondary_key: str | None = None,
272+
) -> None:
273+
"""Initialize the enum cell.
274+
275+
Args:
276+
value (Enum | None): The enum value to display.
277+
justification (Justify): The text justification.
278+
style (str): The style string for the cell.
279+
secondary_key (str | None): An optional secondary string key to use for
280+
tie-breaking during sorting.
281+
"""
282+
283+
display_value = as_enum(value)
284+
primary = display_value.lower() if value is not None else ""
285+
sort_key = (primary, secondary_key.lower()) if secondary_key else (primary,)
286+
super().__init__(sort_key, display_value, justification, style)
287+
288+
289+
class BooleanCell(EnhancedTableCell):
290+
"""Cell that renders boolean values as checkboxes."""
291+
292+
def __init__(
293+
self,
294+
*,
295+
value: bool | None,
296+
justification: Justify = Justify.CENTER,
297+
style: str = "",
298+
secondary_key: str | None = None,
299+
) -> None:
300+
"""Initialize the boolean cell.
301+
302+
Args:
303+
value (bool | None): The boolean value to display.
304+
justification (Justify): The text justification.
305+
style (str): The style string for the cell.
306+
secondary_key (str | None): An optional secondary string key to use for
307+
tie-breaking during sorting.
308+
"""
309+
310+
safe_value = float("-inf") if value is None else float(value)
311+
super().__init__(
312+
_with_secondary_key(safe_value, secondary_key),
313+
as_bool(value=value),
314+
justification,
315+
style,
316+
)
317+
318+
187319
def _get_style_for_value(value: float) -> str:
188320
"""Get the style string based on the sign of a value.
189321
@@ -391,6 +523,90 @@ def _get_style_for_value(value: float) -> str:
391523
),
392524
)
393525
),
526+
"dividend_date": (
527+
quote_column(
528+
"Div Date",
529+
full_name="Dividend Date",
530+
width=10,
531+
key="dividend_date",
532+
justification=Justify.LEFT,
533+
cell_factory=lambda q: DateCell(
534+
q.dividend_date,
535+
justification=Justify.LEFT,
536+
secondary_key=q.symbol or "",
537+
),
538+
)
539+
),
540+
"market_state": (
541+
quote_column(
542+
"Mkt State",
543+
full_name="Market State",
544+
width=10,
545+
key="market_state",
546+
justification=Justify.LEFT,
547+
cell_factory=lambda q: EnumCell(
548+
q.market_state,
549+
justification=Justify.LEFT,
550+
secondary_key=q.symbol or "",
551+
),
552+
)
553+
),
554+
"option_type": (
555+
quote_column(
556+
"Opt Type",
557+
full_name="Option Type",
558+
width=8,
559+
key="option_type",
560+
justification=Justify.LEFT,
561+
cell_factory=lambda q: EnumCell(
562+
q.option_type,
563+
justification=Justify.LEFT,
564+
secondary_key=q.symbol or "",
565+
),
566+
)
567+
),
568+
"quote_type": (
569+
quote_column(
570+
"Type",
571+
full_name="Quote Type",
572+
width=15,
573+
key="quote_type",
574+
justification=Justify.LEFT,
575+
cell_factory=lambda q: EnumCell(
576+
q.quote_type,
577+
justification=Justify.LEFT,
578+
secondary_key=q.symbol or "",
579+
),
580+
)
581+
),
582+
"tradeable": (
583+
quote_column(
584+
"Tradeable",
585+
full_name="Tradeable",
586+
width=9,
587+
key="tradeable",
588+
justification=Justify.CENTER,
589+
cell_factory=lambda q: BooleanCell(
590+
value=q.tradeable,
591+
justification=Justify.CENTER,
592+
secondary_key=q.symbol or "",
593+
),
594+
)
595+
),
596+
"post_market_datetime": (
597+
quote_column(
598+
"Post Mkt",
599+
full_name="Post-Market Datetime",
600+
width=16,
601+
key="post_market_datetime",
602+
justification=Justify.LEFT,
603+
cell_factory=lambda q: DateTimeCell(
604+
q.post_market_datetime,
605+
justification=Justify.LEFT,
606+
secondary_key=q.symbol or "",
607+
),
608+
)
609+
),
394610
}
395611
"""
396612
A dictionary that contains QuoteColumns available for the quote table.

src/calahan/yquote.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ class YQuote(BaseModel):
214214
215215
Applies to ALL quotes.
216216
"""
217+
217218
exchange: str
218219
"""
219220
Securities exchange on which the security is traded.

0 commit comments

Comments
 (0)