Skip to content

Commit e8075e9

Browse files
authored
New column types + example columns (#30)
* New column types + example columns Now supporting: - Many enums - date - datetime - bool * Address review comments.
1 parent af02a07 commit e8075e9

File tree

5 files changed

+492
-10
lines changed

5 files changed

+492
-10
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: 223 additions & 8 deletions
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

@@ -63,13 +74,12 @@ def __init__(
6374
"""
6475

6576
primary = value if case_sensitive else value.lower()
66-
if secondary_key:
67-
sort_key = (
68-
primary,
69-
secondary_key if case_sensitive else secondary_key.lower(),
70-
)
71-
else:
72-
sort_key = (primary,)
77+
secondary = (
78+
(secondary_key if case_sensitive else secondary_key.lower())
79+
if secondary_key
80+
else None
81+
)
82+
sort_key = (primary, secondary) if secondary else (primary,)
7383
super().__init__(sort_key, value, justification, style)
7484

7585

@@ -184,6 +194,127 @@ def __init__(
184194
)
185195

186196

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