streamlit-record-views 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- record_views/__init__.py +46 -0
- record_views/card_config.py +292 -0
- record_views/cards/__init__.py +5 -0
- record_views/cards/card.py +161 -0
- record_views/cards/checks.py +109 -0
- record_views/cards/config.py +15 -0
- record_views/cards/payload.py +162 -0
- record_views/cards/view.py +214 -0
- record_views/frames.py +140 -0
- record_views/frontend/build/card-CVoZmsSv.js +5 -0
- record_views/frontend/build/card-qSM6Sbaq.css +1 -0
- record_views/frontend/build/lineage-D35cX-Fx.js +373 -0
- record_views/frontend/build/lineage_flow-Ct86we97.css +1 -0
- record_views/frontend/build/lineage_flow-lKdYZISl.js +102 -0
- record_views/frontend/build/lineage_graph-BdK4bKp0.css +1 -0
- record_views/frontend/build/lineage_graph-CZXSh76F.js +32 -0
- record_views/frontend/build/lineage_sankey-7Xj1W01Q.css +1 -0
- record_views/frontend/build/lineage_sankey-DBs2gu2z.js +450 -0
- record_views/frontend/build/lineage_split-CDQvVHeL.css +1 -0
- record_views/frontend/build/lineage_split-sF5_dobX.js +152 -0
- record_views/frontend/build/lineage_steps-C8_VsF5_.css +1 -0
- record_views/frontend/build/lineage_steps-CHu4Rfn8.js +45 -0
- record_views/frontend/build/lineage_strip-BQoh98Dj.css +1 -0
- record_views/frontend/build/lineage_strip-BxVcJxs9.js +67 -0
- record_views/frontend/build/lineage_timeline-DcAXYiDx.js +140 -0
- record_views/frontend/build/lineage_timeline-ipFB0yhl.css +1 -0
- record_views/frontend/build/lineage_trail-D2-mmLp9.js +71 -0
- record_views/frontend/build/lineage_trail-b36pDgfv.css +1 -0
- record_views/frontend/build/lineage_tree-Bi1Xo7Hq.js +122 -0
- record_views/frontend/build/lineage_tree-DOlF1dRU.css +1 -0
- record_views/frontend/build/variants-C25Axzf4.js +563 -0
- record_views/lineage/__init__.py +28 -0
- record_views/lineage/flow.py +171 -0
- record_views/lineage/graph.py +152 -0
- record_views/lineage/model.py +388 -0
- record_views/lineage/payload.py +224 -0
- record_views/lineage/sankey.py +182 -0
- record_views/lineage/split.py +124 -0
- record_views/lineage/steps.py +131 -0
- record_views/lineage/strip.py +181 -0
- record_views/lineage/timeline.py +136 -0
- record_views/lineage/trail.py +126 -0
- record_views/lineage/tree.py +211 -0
- record_views/lineage/view.py +193 -0
- record_views/link.py +194 -0
- record_views/py.typed +0 -0
- record_views/pyproject.toml +43 -0
- record_views/record.py +212 -0
- record_views/utils.py +79 -0
- streamlit_record_views-0.1.0.dist-info/METADATA +71 -0
- streamlit_record_views-0.1.0.dist-info/RECORD +54 -0
- streamlit_record_views-0.1.0.dist-info/WHEEL +5 -0
- streamlit_record_views-0.1.0.dist-info/licenses/LICENSE +21 -0
- streamlit_record_views-0.1.0.dist-info/top_level.txt +1 -0
record_views/__init__.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from record_views import card_config
|
|
4
|
+
from record_views.cards.card import CardView, card
|
|
5
|
+
from record_views.cards.config import CardSize
|
|
6
|
+
from record_views.frames import RecordStyle, records_from_frame
|
|
7
|
+
from record_views.lineage import (
|
|
8
|
+
LineageEdge,
|
|
9
|
+
LineageGraph,
|
|
10
|
+
lineage_flow,
|
|
11
|
+
lineage_graph,
|
|
12
|
+
lineage_sankey,
|
|
13
|
+
lineage_split,
|
|
14
|
+
lineage_steps,
|
|
15
|
+
lineage_strip,
|
|
16
|
+
lineage_timeline,
|
|
17
|
+
lineage_trail,
|
|
18
|
+
lineage_tree,
|
|
19
|
+
)
|
|
20
|
+
from record_views.link import Link
|
|
21
|
+
from record_views.record import Color, EntityRef, Record, RecordField
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"card",
|
|
25
|
+
"card_config",
|
|
26
|
+
"CardView",
|
|
27
|
+
"LineageEdge",
|
|
28
|
+
"LineageGraph",
|
|
29
|
+
"Link",
|
|
30
|
+
"CardSize",
|
|
31
|
+
"Color",
|
|
32
|
+
"EntityRef",
|
|
33
|
+
"Record",
|
|
34
|
+
"RecordField",
|
|
35
|
+
"RecordStyle",
|
|
36
|
+
"lineage_flow",
|
|
37
|
+
"lineage_graph",
|
|
38
|
+
"lineage_sankey",
|
|
39
|
+
"lineage_split",
|
|
40
|
+
"lineage_steps",
|
|
41
|
+
"lineage_strip",
|
|
42
|
+
"lineage_timeline",
|
|
43
|
+
"lineage_trail",
|
|
44
|
+
"lineage_tree",
|
|
45
|
+
"records_from_frame",
|
|
46
|
+
]
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""Configurations that say how a record looks as a card."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, ClassVar, Literal
|
|
8
|
+
|
|
9
|
+
from record_views.cards.checks import (
|
|
10
|
+
check_choice,
|
|
11
|
+
check_currency,
|
|
12
|
+
check_hide,
|
|
13
|
+
check_max_fields,
|
|
14
|
+
)
|
|
15
|
+
from record_views.cards.config import (
|
|
16
|
+
HEADER_SLOTS,
|
|
17
|
+
STATUS_DISPLAYS,
|
|
18
|
+
HeaderSlot,
|
|
19
|
+
StatusDisplay,
|
|
20
|
+
)
|
|
21
|
+
from record_views.cards.payload import (
|
|
22
|
+
LEDGER_FIELDS,
|
|
23
|
+
PROPERTY_FIELDS,
|
|
24
|
+
STANDARD_FIELDS,
|
|
25
|
+
FieldLimits,
|
|
26
|
+
color_name,
|
|
27
|
+
)
|
|
28
|
+
from record_views.record import Color, RecordField
|
|
29
|
+
|
|
30
|
+
__all__ = ["LedgerCard", "PropertyCard", "RecordCard", "TileCard"]
|
|
31
|
+
|
|
32
|
+
_PROPERTY_LAYOUTS = ("sheet", "chip", "split")
|
|
33
|
+
_LEDGER_LAYOUTS = ("statement", "metric", "split")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True, kw_only=True)
|
|
37
|
+
class _CardConfig:
|
|
38
|
+
"""Holds the options every card configuration shares."""
|
|
39
|
+
|
|
40
|
+
variant: ClassVar[str]
|
|
41
|
+
field_limits: ClassVar[FieldLimits] = STANDARD_FIELDS
|
|
42
|
+
|
|
43
|
+
border: bool = True
|
|
44
|
+
background: bool = False
|
|
45
|
+
status_display: StatusDisplay = "full"
|
|
46
|
+
status_badge: bool = False
|
|
47
|
+
header: HeaderSlot = "status"
|
|
48
|
+
hide: Iterable[str] = ()
|
|
49
|
+
max_fields: int | None = None
|
|
50
|
+
color: Color | str | None = None
|
|
51
|
+
icon: str | None = None
|
|
52
|
+
status_color: Color | str | None = None
|
|
53
|
+
headline: str | RecordField | tuple[str, Any] | None = None
|
|
54
|
+
|
|
55
|
+
def __post_init__(self) -> None:
|
|
56
|
+
check_choice("header", self.header, HEADER_SLOTS)
|
|
57
|
+
check_choice("status_display", self.status_display, STATUS_DISPLAYS)
|
|
58
|
+
check_max_fields(self.max_fields)
|
|
59
|
+
object.__setattr__(self, "hide", check_hide(self.hide))
|
|
60
|
+
for color in (self.color, self.status_color):
|
|
61
|
+
if color is not None:
|
|
62
|
+
color_name(color)
|
|
63
|
+
headline = self.headline
|
|
64
|
+
if isinstance(headline, tuple):
|
|
65
|
+
object.__setattr__(self, "headline", RecordField(*headline))
|
|
66
|
+
elif headline is not None and not isinstance(
|
|
67
|
+
headline, (str, RecordField)
|
|
68
|
+
):
|
|
69
|
+
raise TypeError(
|
|
70
|
+
"headline must be a field name, a RecordField, or a "
|
|
71
|
+
f"(label, value) pair, not {headline!r}."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True, kw_only=True)
|
|
76
|
+
class RecordCard(_CardConfig):
|
|
77
|
+
"""Describes a full card with a header, a body, and a footer.
|
|
78
|
+
|
|
79
|
+
Attributes:
|
|
80
|
+
border: Whether to outline the card with a border.
|
|
81
|
+
background: Whether to fill the card with the secondary background
|
|
82
|
+
color.
|
|
83
|
+
status_display: "full" for the dot and the text, or the badge, which
|
|
84
|
+
reduce to the dot when space runs out; "dot" for only the dot; or
|
|
85
|
+
None to omit the status, as hide=["status"] does.
|
|
86
|
+
status_badge: Whether to render status as a filled badge instead of a
|
|
87
|
+
dot.
|
|
88
|
+
header: Which item ("status", "caption", or None) sits at the right
|
|
89
|
+
of the header. The other moves to the footer, and None puts both
|
|
90
|
+
there. Small cards always draw the caption in their row. At
|
|
91
|
+
compact size, "status" keeps the status on the first line, and
|
|
92
|
+
"caption" or None moves it to the last line.
|
|
93
|
+
hide: Names of elements to omit, any of "status", "subtitle",
|
|
94
|
+
"caption", and "note". Any iterable of names, kept as a tuple.
|
|
95
|
+
max_fields: The number of fields at medium and large size. None shows
|
|
96
|
+
2 at medium size and all at large size. Compact and small sizes
|
|
97
|
+
ignore it and show none.
|
|
98
|
+
color: Overrides the record's color.
|
|
99
|
+
icon: Overrides the record's icon.
|
|
100
|
+
status_color: Overrides the record's status color.
|
|
101
|
+
headline: The field to lead with, or None. A str names a field of the
|
|
102
|
+
record, a RecordField or (label, value) pair gives the field
|
|
103
|
+
itself, and a name that matches no field leads with nothing. At
|
|
104
|
+
small and compact size the headline sits beside the status, and
|
|
105
|
+
beside the last line, showing only its value for a str and its
|
|
106
|
+
label and value for a RecordField. At medium and large size it
|
|
107
|
+
is the first field, which moves to the front of the fields if
|
|
108
|
+
the record has it and is added in front if not.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
TypeError: If hide is a string, or headline is not a string, a
|
|
112
|
+
RecordField, a (label, value) pair, or None.
|
|
113
|
+
ValueError: If header or status_display is not a supported value,
|
|
114
|
+
max_fields is negative or not an integer, hide contains an
|
|
115
|
+
unknown name, or color or status_color is not a supported color.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
variant: ClassVar[str] = "record"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass(frozen=True, kw_only=True)
|
|
122
|
+
class TileCard(_CardConfig):
|
|
123
|
+
"""Describes a list-row card led by an accent icon avatar.
|
|
124
|
+
|
|
125
|
+
Attributes:
|
|
126
|
+
border: Whether to outline the card with a border.
|
|
127
|
+
background: Whether to fill the card with the secondary background
|
|
128
|
+
color.
|
|
129
|
+
status_display: "full" for the dot and the text, or the badge, which
|
|
130
|
+
reduce to the dot when space runs out; "dot" for only the dot; or
|
|
131
|
+
None to omit the status, as hide=["status"] does.
|
|
132
|
+
status_badge: Whether to render status as a filled badge instead of a
|
|
133
|
+
dot.
|
|
134
|
+
header: Which item ("status", "caption", or None) sits at the right
|
|
135
|
+
of the title line. The other moves to the footer, and None puts
|
|
136
|
+
both there. Small cards always draw the caption in their row.
|
|
137
|
+
hide: Names of elements to omit, any of "status", "subtitle",
|
|
138
|
+
"caption", and "note". Any iterable of names, kept as a tuple.
|
|
139
|
+
max_fields: The number of fields at medium and large size. None shows
|
|
140
|
+
2 at medium size and all at large size. Compact and small sizes
|
|
141
|
+
ignore it and show none.
|
|
142
|
+
color: Overrides the record's color.
|
|
143
|
+
icon: Overrides the record's icon.
|
|
144
|
+
status_color: Overrides the record's status color.
|
|
145
|
+
headline: The field to lead with, or None. A str names a field of the
|
|
146
|
+
record, a RecordField or (label, value) pair gives the field
|
|
147
|
+
itself, and a name that matches no field leads with nothing. At
|
|
148
|
+
small and compact size the headline sits beside the status, and
|
|
149
|
+
beside the last line, showing only its value for a str and its
|
|
150
|
+
label and value for a RecordField. At medium and large size it
|
|
151
|
+
is the first field, which moves to the front of the fields if
|
|
152
|
+
the record has it and is added in front if not.
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
TypeError: If hide is a string, or headline is not a string, a
|
|
156
|
+
RecordField, a (label, value) pair, or None.
|
|
157
|
+
ValueError: If header or status_display is not a supported value,
|
|
158
|
+
max_fields is negative or not an integer, hide contains an
|
|
159
|
+
unknown name, or color or status_color is not a supported color.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
variant: ClassVar[str] = "tile"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass(frozen=True, kw_only=True)
|
|
166
|
+
class PropertyCard(_CardConfig):
|
|
167
|
+
"""Describes an attribute-forward card that lays fields out as a grid.
|
|
168
|
+
|
|
169
|
+
Attributes:
|
|
170
|
+
border: Whether to outline the card with a border.
|
|
171
|
+
background: Whether to fill the card with the secondary background
|
|
172
|
+
color.
|
|
173
|
+
status_display: "full" for the dot and the text, or the badge, which
|
|
174
|
+
reduce to the dot when space runs out; "dot" for only the dot; or
|
|
175
|
+
None to omit the status, as hide=["status"] does.
|
|
176
|
+
status_badge: Whether to render status as a filled badge instead of a
|
|
177
|
+
dot.
|
|
178
|
+
header: Which item ("status", "caption", or None) sits at the right
|
|
179
|
+
of the type line. The other moves to the footer, and None puts
|
|
180
|
+
both there. Small cards always draw the caption in their row. At
|
|
181
|
+
compact size, "status" keeps the status on the first line, and
|
|
182
|
+
"caption" or None moves it to the last line.
|
|
183
|
+
hide: Names of elements to omit, any of "status", "subtitle",
|
|
184
|
+
"caption", and "note". Any iterable of names, kept as a tuple.
|
|
185
|
+
max_fields: The number of fields at medium and large size. None shows
|
|
186
|
+
4 at medium size and all at large size. Compact size ignores it
|
|
187
|
+
and shows the first field; small size shows none.
|
|
188
|
+
color: Overrides the record's color.
|
|
189
|
+
icon: Overrides the record's icon.
|
|
190
|
+
status_color: Overrides the record's status color.
|
|
191
|
+
headline: The field to lead with, or None. A str names a field of the
|
|
192
|
+
record, a RecordField or (label, value) pair gives the field
|
|
193
|
+
itself, and a name that matches no field leads with nothing. At
|
|
194
|
+
small and compact size the headline sits beside the status, and
|
|
195
|
+
beside the last line, showing only its value for a str and its
|
|
196
|
+
label and value for a RecordField. At medium and large size it
|
|
197
|
+
is the first field, which moves to the front of the fields if
|
|
198
|
+
the record has it and is added in front if not.
|
|
199
|
+
layout: Arrangement of the card at medium and large size. "sheet"
|
|
200
|
+
leads with the title over a muted type and status line, "chip"
|
|
201
|
+
leads with a colored type chip, and "split" places the identity
|
|
202
|
+
beside the spec grid when there is room, with the description
|
|
203
|
+
filling the space beside the specs.
|
|
204
|
+
description: Long-form text such as a memo, shown at medium and large
|
|
205
|
+
size in a block cut to a few lines that fades when there is more.
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
TypeError: If hide is a string, or headline is not a string, a
|
|
209
|
+
RecordField, a (label, value) pair, or None.
|
|
210
|
+
ValueError: If header, status_display, or layout is not a supported
|
|
211
|
+
value, max_fields is negative or not an integer, hide contains
|
|
212
|
+
an unknown name, or color or status_color is not a supported
|
|
213
|
+
color.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
variant: ClassVar[str] = "property"
|
|
217
|
+
field_limits: ClassVar[FieldLimits] = PROPERTY_FIELDS
|
|
218
|
+
|
|
219
|
+
layout: Literal["sheet", "chip", "split"] = "sheet"
|
|
220
|
+
description: str | None = None
|
|
221
|
+
|
|
222
|
+
def __post_init__(self) -> None:
|
|
223
|
+
super().__post_init__()
|
|
224
|
+
check_choice("layout", self.layout, _PROPERTY_LAYOUTS)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@dataclass(frozen=True, kw_only=True)
|
|
228
|
+
class LedgerCard(_CardConfig):
|
|
229
|
+
"""Describes a quantified card led by a headline figure.
|
|
230
|
+
|
|
231
|
+
Attributes:
|
|
232
|
+
border: Whether to outline the card with a border.
|
|
233
|
+
background: Whether to fill the card with the secondary background
|
|
234
|
+
color.
|
|
235
|
+
status_display: "full" for the dot and the text, or the badge, which
|
|
236
|
+
reduce to the dot when space runs out; "dot" for only the dot; or
|
|
237
|
+
None to omit the status, as hide=["status"] does.
|
|
238
|
+
status_badge: Whether to render status as a filled badge instead of a
|
|
239
|
+
dot.
|
|
240
|
+
header: Which item ("status", "caption", or None) sits at the right
|
|
241
|
+
of the type line. The other moves to the footer, and None puts
|
|
242
|
+
both there. Small cards always draw the caption in their row. At
|
|
243
|
+
compact size, "status" keeps the status on the first line, and
|
|
244
|
+
"caption" or None moves it to the last line.
|
|
245
|
+
hide: Names of elements to omit, any of "status", "subtitle",
|
|
246
|
+
"caption", and "note". Any iterable of names, kept as a tuple.
|
|
247
|
+
max_fields: The number of itemized fields at medium and large size.
|
|
248
|
+
None shows 3 at medium size and all at large size. Compact and
|
|
249
|
+
small sizes ignore it and show none.
|
|
250
|
+
color: Overrides the record's color.
|
|
251
|
+
icon: Overrides the record's icon.
|
|
252
|
+
status_color: Overrides the record's status color.
|
|
253
|
+
headline: The figure the card leads with, or None for no figure. A
|
|
254
|
+
str names a field of the record, a RecordField or (label, value)
|
|
255
|
+
pair gives the figure itself, and a name that matches no field
|
|
256
|
+
leads with nothing. Medium and large cards draw it as a large
|
|
257
|
+
figure taken out of the itemized fields, which are the numeric
|
|
258
|
+
fields; text fields are listed below them. Small and compact
|
|
259
|
+
cards place it beside the status and on the last line, showing
|
|
260
|
+
only its value for a str and its label and value for a
|
|
261
|
+
RecordField.
|
|
262
|
+
layout: Arrangement of the card at medium and large size.
|
|
263
|
+
"statement" itemizes fields as receipt lines, "metric" shows them
|
|
264
|
+
as stat blocks under the headline, and "split" places the
|
|
265
|
+
itemization beside the headline when there is room.
|
|
266
|
+
currency: An ISO 4217 code, such as "USD", that formats numeric
|
|
267
|
+
values as currency in the viewer's locale. None formats them as
|
|
268
|
+
plain numbers.
|
|
269
|
+
accounting: Whether to present numeric values in accounting style:
|
|
270
|
+
currency symbols and amounts in aligned columns, and negatives in
|
|
271
|
+
parentheses.
|
|
272
|
+
|
|
273
|
+
Raises:
|
|
274
|
+
TypeError: If hide is a string, or headline is not a string, a
|
|
275
|
+
RecordField, a (label, value) pair, or None.
|
|
276
|
+
ValueError: If header, status_display, or layout is not a supported
|
|
277
|
+
value, max_fields is negative or not an integer, hide contains
|
|
278
|
+
an unknown name, color or status_color is not a supported color,
|
|
279
|
+
or currency is not a three-letter code.
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
variant: ClassVar[str] = "ledger"
|
|
283
|
+
field_limits: ClassVar[FieldLimits] = LEDGER_FIELDS
|
|
284
|
+
|
|
285
|
+
layout: Literal["statement", "metric", "split"] = "statement"
|
|
286
|
+
currency: str | None = None
|
|
287
|
+
accounting: bool = False
|
|
288
|
+
|
|
289
|
+
def __post_init__(self) -> None:
|
|
290
|
+
super().__post_init__()
|
|
291
|
+
check_choice("layout", self.layout, _LEDGER_LAYOUTS)
|
|
292
|
+
object.__setattr__(self, "currency", check_currency(self.currency))
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from dataclasses import KW_ONLY, dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from record_views.cards.checks import check_choice, check_width
|
|
8
|
+
from record_views.cards.config import (
|
|
9
|
+
CARD_SIZES,
|
|
10
|
+
HOVER_SIZES,
|
|
11
|
+
ON_HOVER,
|
|
12
|
+
CardSize,
|
|
13
|
+
OnHover,
|
|
14
|
+
Width,
|
|
15
|
+
)
|
|
16
|
+
from record_views.cards.view import (
|
|
17
|
+
CardConfig,
|
|
18
|
+
CardSpec,
|
|
19
|
+
render_view,
|
|
20
|
+
resolve_config,
|
|
21
|
+
view_payload,
|
|
22
|
+
)
|
|
23
|
+
from record_views.record import Record
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class CardView:
|
|
28
|
+
"""Defines a card: the record, how it looks, its size, and its hover.
|
|
29
|
+
|
|
30
|
+
A CardView only describes the card. card() renders it.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
record: The record metadata and fields to display.
|
|
34
|
+
card_config: How the record looks: a card_config.RecordCard,
|
|
35
|
+
TileCard, PropertyCard, or LedgerCard object; one of the names
|
|
36
|
+
"record", "tile", "property", and "ledger", which mean that class
|
|
37
|
+
with its defaults; or None for RecordCard(). Kept as the
|
|
38
|
+
configuration object.
|
|
39
|
+
size: Presentation size: "compact", "small", "medium", or "large".
|
|
40
|
+
"compact" is a fixed card that does not adapt to its width;
|
|
41
|
+
lineage components draw records with it.
|
|
42
|
+
on_hover: What hovering or focusing the card shows above the page.
|
|
43
|
+
None shows the same card in full when its content is cut off,
|
|
44
|
+
and nothing otherwise. "small", "medium", or "large" always shows
|
|
45
|
+
the same card at that size. A CardView always shows that card,
|
|
46
|
+
which can have its own record, configuration, and size. "disabled"
|
|
47
|
+
shows nothing.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
TypeError: If card_config is not a supported value.
|
|
51
|
+
ValueError: If size or on_hover is not a supported value, or on_hover
|
|
52
|
+
is a CardView whose size is "compact" or whose own on_hover is not
|
|
53
|
+
None.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
record: Record
|
|
57
|
+
card_config: CardSpec = None
|
|
58
|
+
_: KW_ONLY
|
|
59
|
+
size: CardSize = "medium"
|
|
60
|
+
on_hover: OnHover | CardView = None
|
|
61
|
+
|
|
62
|
+
def __post_init__(self) -> None:
|
|
63
|
+
object.__setattr__(
|
|
64
|
+
self, "card_config", resolve_config(self.card_config)
|
|
65
|
+
)
|
|
66
|
+
check_choice("size", self.size, CARD_SIZES)
|
|
67
|
+
hover = self.on_hover
|
|
68
|
+
if not isinstance(hover, CardView):
|
|
69
|
+
check_choice("on_hover", hover, ON_HOVER)
|
|
70
|
+
elif hover.size not in HOVER_SIZES:
|
|
71
|
+
raise ValueError(
|
|
72
|
+
f"A hover CardView's size must be one of {HOVER_SIZES}."
|
|
73
|
+
)
|
|
74
|
+
elif hover.on_hover is not None:
|
|
75
|
+
raise ValueError("A hover CardView cannot have its own on_hover.")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _drawn(
|
|
79
|
+
config: CardConfig, record: Record, size: CardSize, disabled: bool
|
|
80
|
+
) -> dict[str, Any]:
|
|
81
|
+
"""Builds the payload of one card, with the variant that draws it."""
|
|
82
|
+
payload = view_payload(config, record, size, disabled=disabled)
|
|
83
|
+
payload["variant"] = config.variant
|
|
84
|
+
return payload
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def card(
|
|
88
|
+
view: CardView,
|
|
89
|
+
*,
|
|
90
|
+
key: str | None = None,
|
|
91
|
+
width: Width = "stretch",
|
|
92
|
+
on_click: Callable[..., Any] | None = None,
|
|
93
|
+
args: tuple[Any, ...] | None = None,
|
|
94
|
+
kwargs: dict[str, Any] | None = None,
|
|
95
|
+
disabled: bool = False,
|
|
96
|
+
help: str | None = None,
|
|
97
|
+
) -> bool:
|
|
98
|
+
"""Renders a card.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
view: The card to render: its record, configuration, size, and what
|
|
102
|
+
hovering it shows.
|
|
103
|
+
key: A unique component key. Defaults to one derived from the card
|
|
104
|
+
type and the record's id, and is required when the record has no
|
|
105
|
+
id.
|
|
106
|
+
width: "stretch" to fill the container, "content" to fit the card's
|
|
107
|
+
content, or a width in pixels.
|
|
108
|
+
on_click: Callback executed when the card is clicked.
|
|
109
|
+
args: Positional arguments passed to on_click.
|
|
110
|
+
kwargs: Keyword arguments passed to on_click.
|
|
111
|
+
disabled: When True, renders the card, and its hover card, in an
|
|
112
|
+
inactive non-interactive state.
|
|
113
|
+
help: Plain text shown in a tooltip when hovering or focusing the
|
|
114
|
+
card. Newlines are preserved.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
True if the card was clicked in the current script run, False otherwise.
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
TypeError: If view is not a CardView.
|
|
121
|
+
ValueError: If width is not a supported value, or key is omitted for
|
|
122
|
+
a record without an id.
|
|
123
|
+
"""
|
|
124
|
+
if not isinstance(view, CardView):
|
|
125
|
+
raise TypeError(
|
|
126
|
+
f"view must be a CardView, such as CardView(record), not {view!r}."
|
|
127
|
+
)
|
|
128
|
+
config, record = view.card_config, view.record
|
|
129
|
+
check_width(width)
|
|
130
|
+
if key is None:
|
|
131
|
+
if record.id is None:
|
|
132
|
+
raise ValueError("key is required when the record has no id.")
|
|
133
|
+
key = f"card-{config.variant}:{record.id}"
|
|
134
|
+
|
|
135
|
+
hover = view.on_hover
|
|
136
|
+
if isinstance(hover, CardView):
|
|
137
|
+
# A hover card always shows, as a hover size does.
|
|
138
|
+
mode = hover.size
|
|
139
|
+
shown = _drawn(hover.card_config, hover.record, hover.size, disabled)
|
|
140
|
+
else:
|
|
141
|
+
mode = hover
|
|
142
|
+
shown = (
|
|
143
|
+
_drawn(config, record, hover, disabled)
|
|
144
|
+
if hover in HOVER_SIZES
|
|
145
|
+
else None
|
|
146
|
+
)
|
|
147
|
+
payload = _drawn(config, record, view.size, disabled)
|
|
148
|
+
payload.update(
|
|
149
|
+
help=help,
|
|
150
|
+
contentWidth=width == "content",
|
|
151
|
+
onHover=mode,
|
|
152
|
+
hover=shown,
|
|
153
|
+
)
|
|
154
|
+
return render_view(
|
|
155
|
+
payload,
|
|
156
|
+
key=key,
|
|
157
|
+
width=width,
|
|
158
|
+
on_click=on_click,
|
|
159
|
+
args=args,
|
|
160
|
+
kwargs=kwargs,
|
|
161
|
+
)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from record_views.cards.config import HIDEABLE, Width
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def check_choice(name: str, value: Any, choices: tuple[Any, ...]) -> None:
|
|
11
|
+
"""Validates that an argument is one of a fixed set of values.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
name: The argument name, for the error message.
|
|
15
|
+
value: The value given.
|
|
16
|
+
choices: The accepted values.
|
|
17
|
+
|
|
18
|
+
Raises:
|
|
19
|
+
ValueError: If value is not one of choices.
|
|
20
|
+
"""
|
|
21
|
+
if value not in choices:
|
|
22
|
+
raise ValueError(f"{name} must be one of {choices}.")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def check_width(width: Width) -> None:
|
|
26
|
+
"""Validates a Streamlit-style width.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
width: "stretch", "content", or a positive number of pixels.
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
ValueError: If width is none of those.
|
|
33
|
+
"""
|
|
34
|
+
if width in ("stretch", "content"):
|
|
35
|
+
return
|
|
36
|
+
if isinstance(width, bool) or not isinstance(width, int) or width <= 0:
|
|
37
|
+
raise ValueError(
|
|
38
|
+
f'width must be "stretch", "content", or a positive integer, '
|
|
39
|
+
f"not {width!r}."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def check_hide(hide: Iterable[str]) -> tuple[str, ...]:
|
|
44
|
+
"""Validates the names of elements to omit from a card.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
hide: Names of elements to omit.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
The names as a tuple.
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
TypeError: If hide is a string.
|
|
54
|
+
ValueError: If hide contains an unknown name.
|
|
55
|
+
"""
|
|
56
|
+
if isinstance(hide, str):
|
|
57
|
+
raise TypeError(
|
|
58
|
+
f"hide must be a list of names, such as [{hide!r}], not a string."
|
|
59
|
+
)
|
|
60
|
+
names = tuple(hide)
|
|
61
|
+
if unknown := frozenset(names) - HIDEABLE:
|
|
62
|
+
raise ValueError(
|
|
63
|
+
f"Cannot hide {sorted(unknown)}; choose from {sorted(HIDEABLE)}."
|
|
64
|
+
)
|
|
65
|
+
return names
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def check_max_fields(max_fields: int | None) -> None:
|
|
69
|
+
"""Validates the number of fields a card shows.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
max_fields: None, or a count of 0 or more.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
ValueError: If max_fields is negative or not an integer.
|
|
76
|
+
"""
|
|
77
|
+
if max_fields is not None and (
|
|
78
|
+
isinstance(max_fields, bool)
|
|
79
|
+
or not isinstance(max_fields, int)
|
|
80
|
+
or max_fields < 0
|
|
81
|
+
):
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f"max_fields must be None or an integer of 0 or more, not "
|
|
84
|
+
f"{max_fields!r}."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def check_currency(currency: str | None) -> str | None:
|
|
89
|
+
"""Validates and normalizes an ISO 4217 currency code.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
currency: A three-letter code, or None.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
The code in uppercase, or None.
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
ValueError: If currency is not a three-letter code.
|
|
99
|
+
"""
|
|
100
|
+
if currency is None:
|
|
101
|
+
return None
|
|
102
|
+
if not isinstance(currency, str) or not re.fullmatch(
|
|
103
|
+
r"[A-Za-z]{3}", currency
|
|
104
|
+
):
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f"currency must be an ISO 4217 code such as 'USD', not "
|
|
107
|
+
f"{currency!r}."
|
|
108
|
+
)
|
|
109
|
+
return currency.upper()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal, get_args
|
|
4
|
+
|
|
5
|
+
CardSize = Literal["compact", "small", "medium", "large"]
|
|
6
|
+
HeaderSlot = Literal["status", "caption"] | None
|
|
7
|
+
StatusDisplay = Literal["full", "dot"] | None
|
|
8
|
+
Width = Literal["stretch", "content"] | int
|
|
9
|
+
OnHover = Literal["small", "medium", "large", "disabled"] | None
|
|
10
|
+
CARD_SIZES = get_args(CardSize)
|
|
11
|
+
HEADER_SLOTS = ("status", "caption", None)
|
|
12
|
+
STATUS_DISPLAYS = ("full", "dot", None)
|
|
13
|
+
HOVER_SIZES = ("small", "medium", "large")
|
|
14
|
+
ON_HOVER = (*HOVER_SIZES, "disabled", None)
|
|
15
|
+
HIDEABLE = frozenset({"status", "subtitle", "caption", "note"})
|