slab-cli 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.
- slab_cli/__init__.py +0 -0
- slab_cli/banner.py +92 -0
- slab_cli/client.py +386 -0
- slab_cli/commands/__init__.py +4 -0
- slab_cli/commands/breaks.py +140 -0
- slab_cli/commands/catalog.py +84 -0
- slab_cli/commands/collection.py +676 -0
- slab_cli/commands/custom_sets.py +484 -0
- slab_cli/commands/export.py +129 -0
- slab_cli/commands/lots.py +114 -0
- slab_cli/commands/pricing.py +386 -0
- slab_cli/commands/registry.py +140 -0
- slab_cli/commands/setup.py +24 -0
- slab_cli/config.py +21 -0
- slab_cli/context.py +50 -0
- slab_cli/display.py +1187 -0
- slab_cli/flags.py +189 -0
- slab_cli/help.py +87 -0
- slab_cli/main.py +95 -0
- slab_cli/paging.py +46 -0
- slab_cli/picker.py +78 -0
- slab_cli/prompts.py +549 -0
- slab_cli/sources.py +17 -0
- slab_cli/theme.py +184 -0
- slab_cli-0.1.0.dist-info/METADATA +70 -0
- slab_cli-0.1.0.dist-info/RECORD +28 -0
- slab_cli-0.1.0.dist-info/WHEEL +4 -0
- slab_cli-0.1.0.dist-info/entry_points.txt +3 -0
slab_cli/theme.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""SLAB terminal theme — the CLI's shared visual language.
|
|
2
|
+
|
|
3
|
+
Mirrors the portal's retro card-collector look (`portal/src/app/globals.css`): gold **foil** as the
|
|
4
|
+
hero accent over slate surfaces, green gains / red losses, uppercase pixel-style headings, rounded
|
|
5
|
+
"cards". Every command should render through the `console` and factories here so the whole CLI reads
|
|
6
|
+
as one system — restyle in this one file and it propagates everywhere.
|
|
7
|
+
|
|
8
|
+
Building block cheat-sheet:
|
|
9
|
+
- `console` — the themed Console (import this instead of making your own).
|
|
10
|
+
- `heading(t, note=)` — a left-aligned gold uppercase section header (print it above a card).
|
|
11
|
+
- `slab_table(cols)` — a card-styled table; pass `foil=True` for the gold "hero card" border.
|
|
12
|
+
- `slab_panel(body)` — a card-styled panel (foil border by default).
|
|
13
|
+
- `kv_grid(rows)` — an aligned label/value block (replaces hand-padded strings).
|
|
14
|
+
- `money / signed_money / pct_coverage` — palette-colored value formatters.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from rich import box
|
|
20
|
+
from rich.console import Console, Group, RenderableType
|
|
21
|
+
from rich.panel import Panel
|
|
22
|
+
from rich.table import Table
|
|
23
|
+
from rich.text import Text
|
|
24
|
+
from rich.theme import Theme
|
|
25
|
+
|
|
26
|
+
# --- Palette (hex mirrors portal/src/app/globals.css :theme tokens) --------------------------------
|
|
27
|
+
FOIL = "#f0b429" # gold — the hero accent (foil)
|
|
28
|
+
FOIL_DIM = "#b8860b"
|
|
29
|
+
TEXT = "#e2e8f0"
|
|
30
|
+
TEXT_DIM = "#7a8baa"
|
|
31
|
+
TEXT_MUTED = "#4a5a7a"
|
|
32
|
+
ACCENT = "#60a5fa" # blue
|
|
33
|
+
SUCCESS = "#34d399" # gains
|
|
34
|
+
DANGER = "#ef4444" # losses
|
|
35
|
+
BORDER = "#2a3a5c" # slate card border
|
|
36
|
+
|
|
37
|
+
# Semantic style names — reference as `[slab.foil]…[/]` in markup, or via the helpers below.
|
|
38
|
+
SLAB_THEME = Theme({
|
|
39
|
+
"slab.foil": f"bold {FOIL}",
|
|
40
|
+
"slab.foil_dim": FOIL_DIM,
|
|
41
|
+
"slab.text": TEXT,
|
|
42
|
+
"slab.value": f"bold {TEXT}",
|
|
43
|
+
"slab.label": TEXT_DIM,
|
|
44
|
+
"slab.dim": TEXT_DIM,
|
|
45
|
+
"slab.muted": TEXT_MUTED,
|
|
46
|
+
"slab.accent": ACCENT,
|
|
47
|
+
"slab.gain": f"bold {SUCCESS}",
|
|
48
|
+
"slab.loss": f"bold {DANGER}",
|
|
49
|
+
"slab.heading": f"bold {FOIL}",
|
|
50
|
+
"slab.border": BORDER,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
console = Console(theme=SLAB_THEME)
|
|
54
|
+
|
|
55
|
+
# The card silhouette — rounded + thin, mirroring the portal's rounded-border cards.
|
|
56
|
+
CARD_BOX = box.ROUNDED
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# --- Headers ---------------------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
def heading(text: str, note: str | None = None) -> Text:
|
|
62
|
+
"""A section header in the portal's pixel-heading style: a slim foil marker + uppercase gold
|
|
63
|
+
title, left-aligned, with an optional dim note (e.g. a count) trailing it. The marker gives every
|
|
64
|
+
titled block across the CLI the same on-theme spine. Print it on its own line above the content."""
|
|
65
|
+
t = Text()
|
|
66
|
+
t.append("▍ ", style="slab.foil")
|
|
67
|
+
t.append(text.upper(), style="slab.heading")
|
|
68
|
+
if note:
|
|
69
|
+
t.append(f" {note}", style="slab.dim")
|
|
70
|
+
return t
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# --- Cards (tables + panels) -----------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
def slab_table(columns: list[tuple[str, dict]], *, foil: bool = False) -> Table:
|
|
76
|
+
"""Build a card-styled table. `columns` is a list of `(header, column_kwargs)` — headers are
|
|
77
|
+
uppercased to match the portal's `.table-header` (uppercase, muted). `foil=True` swaps the slate
|
|
78
|
+
border for the gold "hero card" border."""
|
|
79
|
+
table = Table(
|
|
80
|
+
box=CARD_BOX,
|
|
81
|
+
border_style="slab.foil_dim" if foil else "slab.border",
|
|
82
|
+
header_style="slab.label",
|
|
83
|
+
title=None,
|
|
84
|
+
pad_edge=True,
|
|
85
|
+
expand=False,
|
|
86
|
+
)
|
|
87
|
+
for name, kwargs in columns:
|
|
88
|
+
table.add_column(name.upper(), **kwargs)
|
|
89
|
+
return table
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def slab_panel(body: RenderableType, *, foil: bool = True, title: str | None = None) -> Panel:
|
|
93
|
+
"""Wrap a renderable in a card. Foil (gold) border by default — that's the "hero card" the portal
|
|
94
|
+
reserves for headline stats; pass `foil=False` for an ordinary slate card. Padding is tight
|
|
95
|
+
(no blank top/bottom line) so a card hugs its content instead of reading as a near-empty box."""
|
|
96
|
+
return Panel(
|
|
97
|
+
body,
|
|
98
|
+
box=CARD_BOX,
|
|
99
|
+
border_style="slab.foil_dim" if foil else "slab.border",
|
|
100
|
+
title=title,
|
|
101
|
+
title_align="left",
|
|
102
|
+
padding=(0, 2),
|
|
103
|
+
expand=False, # hug content — a card sized to its data reads far better than a half-empty bar
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# --- Sections: the canonical "titled block" every multi-part output composes from -----------------
|
|
108
|
+
|
|
109
|
+
def section(title: str, body: RenderableType, *, note: str | None = None, foil: bool = False) -> Group:
|
|
110
|
+
"""A titled card: a blank line for rhythm, the foil-marked heading, then `body` in a slate (or
|
|
111
|
+
foil) card. THE reusable unit for command output — build every section of every command from this
|
|
112
|
+
and the whole CLI shares one spacing + heading language. Restyle here, everything follows."""
|
|
113
|
+
return Group(Text(), heading(title, note), slab_panel(body, foil=foil))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def titled(title: str, renderable: RenderableType, *, note: str | None = None) -> Group:
|
|
117
|
+
"""Like `section` but for content that draws its own frame (a `slab_table`, a chart): blank line +
|
|
118
|
+
marked heading + the renderable as-is, with no extra card around it. Use for titled tables so
|
|
119
|
+
they get the same spacing + heading as `section` cards without a double border."""
|
|
120
|
+
return Group(Text(), heading(title, note), renderable)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def kv_grid(rows: list[tuple[str, RenderableType]], *, label_style: str = "slab.label") -> Table:
|
|
124
|
+
"""An aligned two-column label/value block — the reusable replacement for hand-padded strings so
|
|
125
|
+
values always line up. Values may be plain strings, markup strings, or Rich renderables."""
|
|
126
|
+
grid = Table.grid(padding=(0, 3))
|
|
127
|
+
grid.add_column(style=label_style, justify="left")
|
|
128
|
+
grid.add_column(justify="left")
|
|
129
|
+
for label, value in rows:
|
|
130
|
+
grid.add_row(label, value)
|
|
131
|
+
return grid
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# --- Value formatters ------------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
def money(v) -> str:
|
|
137
|
+
"""`$1,234.56`, or an em-dash when absent."""
|
|
138
|
+
return f"${v:,.2f}" if v is not None else "—"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def signed_money(v) -> str:
|
|
142
|
+
"""Colored, signed money for P&L: green gain / red loss, em-dash when absent."""
|
|
143
|
+
if v is None:
|
|
144
|
+
return "[slab.dim]—[/]"
|
|
145
|
+
color = "slab.gain" if v >= 0 else "slab.loss"
|
|
146
|
+
sign = "+" if v >= 0 else "-"
|
|
147
|
+
return f"[{color}]{sign}{money(abs(float(v)))}[/]"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def pct_coverage(pct: float) -> str:
|
|
151
|
+
"""A coverage/health percentage, colored by band: green ≥75, gold ≥40, red below."""
|
|
152
|
+
color = "slab.gain" if pct >= 75 else ("slab.foil" if pct >= 40 else "slab.loss")
|
|
153
|
+
return f"[{color}]{pct:.0f}%[/]"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
_SPARK_TICKS = "▁▂▃▄▅▆▇█"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def sparkline(values: list, *, style: str = "slab.foil", width: int = 24) -> str:
|
|
160
|
+
"""A one-line trend as unicode blocks (`▁▂▅▇█`) — the compact CLI stand-in for the portal's
|
|
161
|
+
portfolio chart. Series longer than `width` are evenly downsampled (the dashboard payload's
|
|
162
|
+
embedded trend is daily, ~90 points). Returns markup, or '' when there's nothing to plot
|
|
163
|
+
(<2 points)."""
|
|
164
|
+
nums = [float(v) for v in values if v is not None]
|
|
165
|
+
if len(nums) < 2:
|
|
166
|
+
return ""
|
|
167
|
+
if len(nums) > width:
|
|
168
|
+
step = (len(nums) - 1) / (width - 1)
|
|
169
|
+
nums = [nums[round(i * step)] for i in range(width)]
|
|
170
|
+
lo, hi = min(nums), max(nums)
|
|
171
|
+
span = hi - lo
|
|
172
|
+
if not span:
|
|
173
|
+
ticks = _SPARK_TICKS[3] * len(nums) # dead-flat: a mid line, not empty
|
|
174
|
+
else:
|
|
175
|
+
ticks = "".join(_SPARK_TICKS[min(7, int((v - lo) / span * 7))] for v in nums)
|
|
176
|
+
return f"[{style}]{ticks}[/]"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def bar(value: float, maxv: float, width: int = 14, *, style: str = "slab.foil") -> str:
|
|
180
|
+
"""A compact filled/track meter (`████····`) as a markup string — the reusable bar for
|
|
181
|
+
distributions, completion, and breakdowns. Filled cells take `style`; the track is muted."""
|
|
182
|
+
filled = round((value / maxv) * width) if maxv else 0
|
|
183
|
+
filled = max(0, min(width, filled))
|
|
184
|
+
return f"[{style}]{'█' * filled}[/][slab.muted]{'·' * (width - filled)}[/]"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: slab-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI for the slab trading-card API — catalog, collect, and track your cards from the terminal.
|
|
5
|
+
Author: dev_jeb
|
|
6
|
+
Requires-Dist: slab-schemas>=0.1.0
|
|
7
|
+
Requires-Dist: httpx>=0.27
|
|
8
|
+
Requires-Dist: rich>=13.0
|
|
9
|
+
Requires-Dist: inquirerpy>=0.3
|
|
10
|
+
Requires-Python: >=3.13
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# slab-cli
|
|
14
|
+
|
|
15
|
+
A terminal client for the [slab](https://api.slab.dev-jeb.com) trading-card API — browse the
|
|
16
|
+
catalog, price cards against real sales comps, and track your own collection from the command line.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install slab-cli
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This installs the `slab` command.
|
|
23
|
+
|
|
24
|
+
## Setup
|
|
25
|
+
|
|
26
|
+
The CLI talks to the hosted slab API. You need an **API key** — mint one from the
|
|
27
|
+
[slab portal](https://app.slab.dev-jeb.com) (sign in → **API Keys** → create). Then:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
export SLAB_API_KEY=sk_live_... # your key (required)
|
|
31
|
+
# optional:
|
|
32
|
+
export SLAB_API_URL=https://api.slab.dev-jeb.com # default; override for a self-hosted API
|
|
33
|
+
export SLAB_COLLECTOR=<collector-uuid> # your active collector, reused across commands
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quickstart
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Catalog + pricing (no collector needed)
|
|
40
|
+
slab card search --subject "Connor Bedard" # find cards
|
|
41
|
+
slab card price <card-uuid> # fair-market value from sales comps
|
|
42
|
+
slab card comps <card-uuid> # the underlying recent sales
|
|
43
|
+
slab set "OPC Platinum" # a set's sealed FMV + top cards
|
|
44
|
+
|
|
45
|
+
# Your collection
|
|
46
|
+
slab collector create # one-time: create your collector profile
|
|
47
|
+
slab collection add # add an owned copy (grade, cost, acquisition)
|
|
48
|
+
slab collection cost # log grading/shipping costs on a copy
|
|
49
|
+
slab break new # record a box/case break
|
|
50
|
+
slab collection dashboard # portfolio value, cost basis, P&L
|
|
51
|
+
slab collection search # search + filter your copies
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Run `slab` with no arguments for the full command list, or `slab <group>` (e.g. `slab collection`)
|
|
55
|
+
to see a group's verbs.
|
|
56
|
+
|
|
57
|
+
## Configuration reference
|
|
58
|
+
|
|
59
|
+
| Env var | Purpose | Default |
|
|
60
|
+
| --------------- | ------------------------------------------ | -------------------------------- |
|
|
61
|
+
| `SLAB_API_KEY` | API key for authentication (**required**) | — |
|
|
62
|
+
| `SLAB_API_URL` | Base URL of the slab API | `https://api.slab.dev-jeb.com` |
|
|
63
|
+
| `SLAB_COLLECTOR`| Active collector UUID, reused per command | — |
|
|
64
|
+
|
|
65
|
+
## What it depends on
|
|
66
|
+
|
|
67
|
+
Requests and responses are typed against [`slab-schemas`](https://pypi.org/project/slab-schemas/) —
|
|
68
|
+
the same pydantic wire contract the API itself uses — so the CLI and API can never drift.
|
|
69
|
+
|
|
70
|
+
Requires Python 3.13+.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
slab_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
slab_cli/banner.py,sha256=Y8daw7PNgC2LL4Gp0CDtqPNzEYAMEpWN5WqpKmMY8NU,3690
|
|
3
|
+
slab_cli/client.py,sha256=QzRW0qqd_yf3DF7GytMkPuApu9cshvE2fFuYyKYhRwg,15928
|
|
4
|
+
slab_cli/commands/__init__.py,sha256=Mzy3oCKEj9ldoTSekQhaIpaSEVr8AmN05ogV1MqmQnM,237
|
|
5
|
+
slab_cli/commands/breaks.py,sha256=hV-wghXIxjIZWghFtPmIt2iCDeEGy85tHx4C2flcZPo,5553
|
|
6
|
+
slab_cli/commands/catalog.py,sha256=cCBFFcz6n-cMXE_nd7l8_iHXP0EBr5JGPOoc6LqS9gw,3092
|
|
7
|
+
slab_cli/commands/collection.py,sha256=dwqBemFtpDaYRNn2U2SwryMap3tQvUiBspkZzTk1BR4,28429
|
|
8
|
+
slab_cli/commands/custom_sets.py,sha256=lQVOqKlppTVduPbMQ5Y524jS9RnI3zGjqfMxTt7Ml6c,18639
|
|
9
|
+
slab_cli/commands/export.py,sha256=Gqv1q0bsB1AyI--_N-k2GsIWwvl_DZqTCz08CkR37QI,5236
|
|
10
|
+
slab_cli/commands/lots.py,sha256=btRzZdxINbsgRFdGwednaTp5fkUMX0iq52vFgVf5v2w,4969
|
|
11
|
+
slab_cli/commands/pricing.py,sha256=bW_c23DF0MKueU73eGLUrIdc2w7Xi9FlwivlQGvJKng,14522
|
|
12
|
+
slab_cli/commands/registry.py,sha256=amLA2_jpLIutam3HGiKhKZgk17z9-4RsP1PBaDDeIHw,7323
|
|
13
|
+
slab_cli/commands/setup.py,sha256=sI3ML7KJNMpjWg4qUVNAgUXYwvlPs8KMAl7zaN-01hA,848
|
|
14
|
+
slab_cli/config.py,sha256=k-g0LNd1QZL_bq_fdPtI-_gp928kt7ECeMEk8AzeUpE,546
|
|
15
|
+
slab_cli/context.py,sha256=bv8M7qw0GAObBlmG0pY7iAWVtDf9LRTvVfoG4HNvPBw,1634
|
|
16
|
+
slab_cli/display.py,sha256=zC5OlI-9_XmfFTMAh-nv-oyYS5Eqtj2X_41B1_LjHAU,49269
|
|
17
|
+
slab_cli/flags.py,sha256=q7o4EEPCkGu4w4VXgoY2ZOGybeV0r5IdCB61fS8KCBI,8550
|
|
18
|
+
slab_cli/help.py,sha256=ByGAY9ncvnzU8AlkXeFdkvIOFAZn_2yMLrgLnssZUTI,3706
|
|
19
|
+
slab_cli/main.py,sha256=FRqPAJZ3wbI18BKfO3xFIwr_U5-izeo0qRbGEHI6nC4,3623
|
|
20
|
+
slab_cli/paging.py,sha256=P02o6GY3ewB8Xnj-8EAGddKnWspil8T1-qC13lpOC1c,1825
|
|
21
|
+
slab_cli/picker.py,sha256=-8emLfy7TsKnHhEKQAA27o-9nQiBUy02btWSZ0flQwI,3248
|
|
22
|
+
slab_cli/prompts.py,sha256=vy4dwuL3ed3V5Yqn9QbX9SpYUUXMNw56WX8PymLXGFE,23452
|
|
23
|
+
slab_cli/sources.py,sha256=rmmA6CTBfZP6IrTRvbGqC5Hd53Eo_LRXPQInXDeCE_M,890
|
|
24
|
+
slab_cli/theme.py,sha256=AxeadKoij-KzN_vyQB08yKzYOeleK71A7c2tAu3ffzA,8084
|
|
25
|
+
slab_cli-0.1.0.dist-info/WHEEL,sha256=iHtWm8nRfs0VRdCYVXocAWFW8ppjHL-uTJkAdZJKOBM,80
|
|
26
|
+
slab_cli-0.1.0.dist-info/entry_points.txt,sha256=R5AFsQ2rL_F_OKAT75PeytsSHtl3BR4eTK8fpJR0yr0,45
|
|
27
|
+
slab_cli-0.1.0.dist-info/METADATA,sha256=WrqhEAItcTm1jgm4LhEuU7PO_7DikLPmelemEpme3JY,2904
|
|
28
|
+
slab_cli-0.1.0.dist-info/RECORD,,
|