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.
Files changed (54) hide show
  1. record_views/__init__.py +46 -0
  2. record_views/card_config.py +292 -0
  3. record_views/cards/__init__.py +5 -0
  4. record_views/cards/card.py +161 -0
  5. record_views/cards/checks.py +109 -0
  6. record_views/cards/config.py +15 -0
  7. record_views/cards/payload.py +162 -0
  8. record_views/cards/view.py +214 -0
  9. record_views/frames.py +140 -0
  10. record_views/frontend/build/card-CVoZmsSv.js +5 -0
  11. record_views/frontend/build/card-qSM6Sbaq.css +1 -0
  12. record_views/frontend/build/lineage-D35cX-Fx.js +373 -0
  13. record_views/frontend/build/lineage_flow-Ct86we97.css +1 -0
  14. record_views/frontend/build/lineage_flow-lKdYZISl.js +102 -0
  15. record_views/frontend/build/lineage_graph-BdK4bKp0.css +1 -0
  16. record_views/frontend/build/lineage_graph-CZXSh76F.js +32 -0
  17. record_views/frontend/build/lineage_sankey-7Xj1W01Q.css +1 -0
  18. record_views/frontend/build/lineage_sankey-DBs2gu2z.js +450 -0
  19. record_views/frontend/build/lineage_split-CDQvVHeL.css +1 -0
  20. record_views/frontend/build/lineage_split-sF5_dobX.js +152 -0
  21. record_views/frontend/build/lineage_steps-C8_VsF5_.css +1 -0
  22. record_views/frontend/build/lineage_steps-CHu4Rfn8.js +45 -0
  23. record_views/frontend/build/lineage_strip-BQoh98Dj.css +1 -0
  24. record_views/frontend/build/lineage_strip-BxVcJxs9.js +67 -0
  25. record_views/frontend/build/lineage_timeline-DcAXYiDx.js +140 -0
  26. record_views/frontend/build/lineage_timeline-ipFB0yhl.css +1 -0
  27. record_views/frontend/build/lineage_trail-D2-mmLp9.js +71 -0
  28. record_views/frontend/build/lineage_trail-b36pDgfv.css +1 -0
  29. record_views/frontend/build/lineage_tree-Bi1Xo7Hq.js +122 -0
  30. record_views/frontend/build/lineage_tree-DOlF1dRU.css +1 -0
  31. record_views/frontend/build/variants-C25Axzf4.js +563 -0
  32. record_views/lineage/__init__.py +28 -0
  33. record_views/lineage/flow.py +171 -0
  34. record_views/lineage/graph.py +152 -0
  35. record_views/lineage/model.py +388 -0
  36. record_views/lineage/payload.py +224 -0
  37. record_views/lineage/sankey.py +182 -0
  38. record_views/lineage/split.py +124 -0
  39. record_views/lineage/steps.py +131 -0
  40. record_views/lineage/strip.py +181 -0
  41. record_views/lineage/timeline.py +136 -0
  42. record_views/lineage/trail.py +126 -0
  43. record_views/lineage/tree.py +211 -0
  44. record_views/lineage/view.py +193 -0
  45. record_views/link.py +194 -0
  46. record_views/py.typed +0 -0
  47. record_views/pyproject.toml +43 -0
  48. record_views/record.py +212 -0
  49. record_views/utils.py +79 -0
  50. streamlit_record_views-0.1.0.dist-info/METADATA +71 -0
  51. streamlit_record_views-0.1.0.dist-info/RECORD +54 -0
  52. streamlit_record_views-0.1.0.dist-info/WHEEL +5 -0
  53. streamlit_record_views-0.1.0.dist-info/licenses/LICENSE +21 -0
  54. streamlit_record_views-0.1.0.dist-info/top_level.txt +1 -0
@@ -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,5 @@
1
+ from __future__ import annotations
2
+
3
+ from record_views.cards.config import CardSize
4
+
5
+ __all__ = ["CardSize"]
@@ -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"})