fp-cloud-cli 0.0.1b1__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.
- fp_cli/__init__.py +10 -0
- fp_cli/__main__.py +4 -0
- fp_cli/_click_compat.py +64 -0
- fp_cli/_context.py +332 -0
- fp_cli/_version.py +1 -0
- fp_cli/analytics.py +432 -0
- fp_cli/analytics_config.py +77 -0
- fp_cli/analytics_registry.py +83 -0
- fp_cli/app.py +492 -0
- fp_cli/auth.py +160 -0
- fp_cli/client.py +1694 -0
- fp_cli/commands/__init__.py +0 -0
- fp_cli/commands/_write.py +214 -0
- fp_cli/commands/agent_cmds.py +407 -0
- fp_cli/commands/alerts_cmds.py +445 -0
- fp_cli/commands/audits_cmds.py +1054 -0
- fp_cli/commands/auth_cmds.py +512 -0
- fp_cli/commands/errors_cmds.py +190 -0
- fp_cli/commands/evals_cmds.py +161 -0
- fp_cli/commands/events_cmds.py +159 -0
- fp_cli/commands/fleet_cmds.py +416 -0
- fp_cli/commands/guardrails_cmds.py +148 -0
- fp_cli/commands/incidents_cmds.py +472 -0
- fp_cli/commands/keys_cmds.py +407 -0
- fp_cli/commands/list_cmds.py +63 -0
- fp_cli/commands/orgs_cmds.py +319 -0
- fp_cli/commands/policies_cmds.py +499 -0
- fp_cli/commands/queries_cmds.py +378 -0
- fp_cli/commands/sessions_cmds.py +151 -0
- fp_cli/commands/settings_cmds.py +150 -0
- fp_cli/commands/usage_cmds.py +35 -0
- fp_cli/commands/users_cmds.py +404 -0
- fp_cli/config.py +330 -0
- fp_cli/dates.py +78 -0
- fp_cli/enforcement.py +345 -0
- fp_cli/errors.py +98 -0
- fp_cli/models.py +891 -0
- fp_cli/orgs.py +30 -0
- fp_cli/output.py +6593 -0
- fp_cli/permissions.py +208 -0
- fp_cli/policy_check.py +290 -0
- fp_cli/py.typed +0 -0
- fp_cli/select.py +322 -0
- fp_cli/theme.py +53 -0
- fp_cloud_cli-0.0.1b1.dist-info/METADATA +335 -0
- fp_cloud_cli-0.0.1b1.dist-info/RECORD +50 -0
- fp_cloud_cli-0.0.1b1.dist-info/WHEEL +5 -0
- fp_cloud_cli-0.0.1b1.dist-info/entry_points.txt +2 -0
- fp_cloud_cli-0.0.1b1.dist-info/licenses/LICENSE +42 -0
- fp_cloud_cli-0.0.1b1.dist-info/top_level.txt +1 -0
fp_cli/select.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""Interactive selection helpers (TTY pickers), shared across commands.
|
|
2
|
+
|
|
3
|
+
Kept in one place so the org picker used at ``login`` and at ``orgs switch`` reads
|
|
4
|
+
and behaves identically. Pure presentation + input; no network, no persistence.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from typing import List, Optional, Sequence
|
|
12
|
+
|
|
13
|
+
from . import output
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def stdin_is_tty() -> bool: # noqa: D401
|
|
17
|
+
"""True iff stdin is an interactive terminal (so we may prompt for a choice).
|
|
18
|
+
|
|
19
|
+
Factored out so it can be stubbed in tests — the CliRunner's stdin is not a
|
|
20
|
+
TTY, so the interactive pickers are otherwise unreachable under test.
|
|
21
|
+
"""
|
|
22
|
+
try:
|
|
23
|
+
return sys.stdin.isatty()
|
|
24
|
+
except Exception:
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def choose_org(slugs: Sequence[str], default: Optional[str] = None) -> str:
|
|
29
|
+
"""Render ``slugs`` and prompt until the user picks one (by slug or number).
|
|
30
|
+
|
|
31
|
+
``default`` (only if it is one of ``slugs``) is the Enter-to-keep choice and is
|
|
32
|
+
marked ``· current`` in the list. Re-prompts on any out-of-range / unknown input,
|
|
33
|
+
so the return value is always one of ``slugs`` — a non-member slug can never be
|
|
34
|
+
selected through the picker.
|
|
35
|
+
"""
|
|
36
|
+
slugs = list(slugs)
|
|
37
|
+
output.org_picker(slugs, current=default)
|
|
38
|
+
prompt_default = default if (default in slugs) else None
|
|
39
|
+
while True:
|
|
40
|
+
choice = str(output.prompt("org", default=prompt_default)).strip()
|
|
41
|
+
if choice in slugs:
|
|
42
|
+
return choice
|
|
43
|
+
if choice.isdigit() and 1 <= int(choice) <= len(slugs):
|
|
44
|
+
return slugs[int(choice) - 1]
|
|
45
|
+
output.warn(f" '{choice}' is not one of your orgs — try again.")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ── orgs switch — arrow-key picker (raw mode) + numbered fallback ─────────────
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _supports_raw_picker() -> bool:
|
|
52
|
+
"""True iff we can run the raw-mode arrow picker: a real interactive TTY on both stdin
|
|
53
|
+
(we read keys) and stderr (we draw the menu), and POSIX ``termios`` is importable.
|
|
54
|
+
Anything else — pipes, CI, the test runner, Windows — falls back to the numbered prompt
|
|
55
|
+
(so the picker never hangs or crashes off a TTY, per the handoff spec §3/§7)."""
|
|
56
|
+
try:
|
|
57
|
+
import termios # noqa: F401
|
|
58
|
+
except Exception:
|
|
59
|
+
return False
|
|
60
|
+
try:
|
|
61
|
+
return bool(sys.stdin.isatty() and sys.stderr.isatty())
|
|
62
|
+
except Exception:
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _read_key(fd: int) -> str:
|
|
67
|
+
"""Read one logical keypress from a raw-mode ``fd`` → a token: ``UP``/``DOWN``/``ENTER``/
|
|
68
|
+
``ESC``/``EOF`` or the raw character. Distinguishes a lone Esc from an arrow escape
|
|
69
|
+
sequence (``\\x1b[A``) with a tiny ``select`` timeout so Esc never blocks."""
|
|
70
|
+
import select as _sel
|
|
71
|
+
|
|
72
|
+
ch = os.read(fd, 1)
|
|
73
|
+
if not ch:
|
|
74
|
+
return "EOF"
|
|
75
|
+
if ch == b"\x1b": # Esc, or the start of an arrow escape sequence
|
|
76
|
+
r, _, _ = _sel.select([fd], [], [], 0.0008)
|
|
77
|
+
if not r:
|
|
78
|
+
return "ESC"
|
|
79
|
+
seq = os.read(fd, 2)
|
|
80
|
+
return {b"[A": "UP", b"[B": "DOWN", b"[C": "RIGHT", b"[D": "LEFT"}.get(seq, "ESC")
|
|
81
|
+
if ch in (b"\r", b"\n"):
|
|
82
|
+
return "ENTER"
|
|
83
|
+
if ch == b"\x03": # Ctrl-C (also raised as KeyboardInterrupt under cbreak)
|
|
84
|
+
return "CTRL_C"
|
|
85
|
+
return ch.decode("utf-8", "ignore")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _numbered_pick(orgs: Sequence[dict], *, current: Optional[str]) -> str:
|
|
89
|
+
"""Non-TTY fallback: a boxed numbered list + a typed choice (slug or number). Re-prompts on
|
|
90
|
+
bad input, so the return is always one of the orgs' slugs. Default is the current org. With
|
|
91
|
+
no input at all (closed/empty stdin, e.g. CI) the prompt aborts → a clean usage error so the
|
|
92
|
+
run never hangs."""
|
|
93
|
+
import typer
|
|
94
|
+
|
|
95
|
+
from . import _click_compat as click # the Click Typer is running
|
|
96
|
+
|
|
97
|
+
slugs: List[str] = [o["slug"] for o in orgs]
|
|
98
|
+
output.render_org_picker_numbered(orgs, current=current)
|
|
99
|
+
default = current if current in slugs else None
|
|
100
|
+
while True:
|
|
101
|
+
try:
|
|
102
|
+
choice = str(output.prompt("org", default=default)).strip()
|
|
103
|
+
except (click.Abort, EOFError):
|
|
104
|
+
raise typer.BadParameter(
|
|
105
|
+
"No org selected and no interactive terminal. "
|
|
106
|
+
"Pass a slug, e.g. `fp orgs switch <slug>`."
|
|
107
|
+
)
|
|
108
|
+
if choice in slugs:
|
|
109
|
+
return choice
|
|
110
|
+
if choice.isdigit() and 1 <= int(choice) <= len(slugs):
|
|
111
|
+
return slugs[int(choice) - 1]
|
|
112
|
+
output.warn(f" '{choice}' is not one of your orgs — try again.")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def choose_org_interactive(orgs: Sequence[dict], *, current_slug: Optional[str] = None) -> Optional[str]:
|
|
116
|
+
"""Pick an org to switch to. ``orgs`` is a list of ``{"slug", "is_current"}`` dicts. Returns
|
|
117
|
+
the chosen slug, or ``None`` if the user cancelled (Esc / Ctrl-C). On a real TTY this is an
|
|
118
|
+
in-place arrow-key menu (cursor starts on the current org); otherwise it falls back to a
|
|
119
|
+
numbered prompt (which can't cancel — it always returns a slug)."""
|
|
120
|
+
orgs = list(orgs)
|
|
121
|
+
if not _supports_raw_picker():
|
|
122
|
+
return _numbered_pick(orgs, current=current_slug)
|
|
123
|
+
|
|
124
|
+
import termios
|
|
125
|
+
import tty
|
|
126
|
+
|
|
127
|
+
from rich.live import Live
|
|
128
|
+
|
|
129
|
+
idx = next((i for i, o in enumerate(orgs) if o.get("is_current")), 0)
|
|
130
|
+
fd = sys.stdin.fileno()
|
|
131
|
+
old = termios.tcgetattr(fd)
|
|
132
|
+
try:
|
|
133
|
+
tty.setcbreak(fd)
|
|
134
|
+
with Live(output.org_picker_frame(orgs, idx), console=output._stderr,
|
|
135
|
+
auto_refresh=False, transient=True) as live:
|
|
136
|
+
while True:
|
|
137
|
+
key = _read_key(fd)
|
|
138
|
+
if key in ("UP", "k"):
|
|
139
|
+
idx = (idx - 1) % len(orgs)
|
|
140
|
+
elif key in ("DOWN", "j"):
|
|
141
|
+
idx = (idx + 1) % len(orgs)
|
|
142
|
+
elif key == "ENTER":
|
|
143
|
+
return orgs[idx]["slug"]
|
|
144
|
+
elif key in ("ESC", "CTRL_C", "q", "EOF"):
|
|
145
|
+
return None
|
|
146
|
+
else:
|
|
147
|
+
continue
|
|
148
|
+
live.update(output.org_picker_frame(orgs, idx))
|
|
149
|
+
live.refresh()
|
|
150
|
+
except KeyboardInterrupt:
|
|
151
|
+
return None
|
|
152
|
+
finally:
|
|
153
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ── login — the single-box interactive flow (one Live panel, raw-mode in-box input) ──
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class LoginCancelled(Exception):
|
|
160
|
+
"""The user pressed Esc / Ctrl-C during the login flow (calm cancel, not an error)."""
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def login_box_supported() -> bool:
|
|
164
|
+
"""True iff the single-box interactive login can run (real TTY on stdin+stderr + termios) —
|
|
165
|
+
otherwise ``login`` uses the plain prompt flow (tests, pipes, CI, ``--json``)."""
|
|
166
|
+
return _supports_raw_picker()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class LoginBox:
|
|
170
|
+
"""Drives the single-box ``login``: ONE Rich ``Live`` panel redrawn in place as steps advance.
|
|
171
|
+
email/code are read in **raw mode** (echo off) and rendered INSIDE the frame; the org step is the
|
|
172
|
+
shared arrow picker as a nested inset. Restores the terminal on exit. Used only on a real TTY —
|
|
173
|
+
the non-interactive path keeps the plain prompts."""
|
|
174
|
+
|
|
175
|
+
def __init__(self) -> None:
|
|
176
|
+
self.done: List = [] # collapsed ✓ steps: (label, value)
|
|
177
|
+
self.active: Optional[str] = None
|
|
178
|
+
self._fd = sys.stdin.fileno()
|
|
179
|
+
self._old = None
|
|
180
|
+
self._live = None
|
|
181
|
+
|
|
182
|
+
def __enter__(self) -> "LoginBox":
|
|
183
|
+
import termios
|
|
184
|
+
import tty
|
|
185
|
+
|
|
186
|
+
from rich.live import Live
|
|
187
|
+
|
|
188
|
+
self._old = termios.tcgetattr(self._fd)
|
|
189
|
+
tty.setcbreak(self._fd) # canonical + echo off, signals on (Ctrl-C → KeyboardInterrupt)
|
|
190
|
+
self._live = Live(output.render_login_frame(self.done, None), console=output._stderr,
|
|
191
|
+
auto_refresh=False, transient=False)
|
|
192
|
+
self._live.__enter__()
|
|
193
|
+
return self
|
|
194
|
+
|
|
195
|
+
def __exit__(self, *exc) -> None:
|
|
196
|
+
import termios
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
if self._live is not None:
|
|
200
|
+
self._live.__exit__(*exc)
|
|
201
|
+
finally:
|
|
202
|
+
termios.tcsetattr(self._fd, termios.TCSADRAIN, self._old)
|
|
203
|
+
|
|
204
|
+
def _draw(self, **kw) -> None:
|
|
205
|
+
self._live.update(output.render_login_frame(self.done, self.active, **kw))
|
|
206
|
+
self._live.refresh()
|
|
207
|
+
|
|
208
|
+
def _read_line(self, *, helper, slots, error) -> str:
|
|
209
|
+
"""Read one line in raw mode, redrawing the frame on every keystroke so the typed text
|
|
210
|
+
appears INSIDE the box (echo is off). Backspace edits; Enter accepts; Esc / Ctrl-C / EOF
|
|
211
|
+
cancel (an arrow escape-sequence mid-type is swallowed, not a cancel). A ``slots`` field
|
|
212
|
+
accepts digits only, capped at ``slots``."""
|
|
213
|
+
import select as _sel
|
|
214
|
+
|
|
215
|
+
buf = ""
|
|
216
|
+
self._draw(active_value=buf, active_slots=slots, helper=helper, error=error)
|
|
217
|
+
while True:
|
|
218
|
+
try:
|
|
219
|
+
ch = os.read(self._fd, 1)
|
|
220
|
+
except KeyboardInterrupt:
|
|
221
|
+
raise LoginCancelled()
|
|
222
|
+
if not ch:
|
|
223
|
+
raise LoginCancelled()
|
|
224
|
+
if ch in (b"\r", b"\n"):
|
|
225
|
+
return buf
|
|
226
|
+
if ch == b"\x03": # Ctrl-C as a byte (if signals were off)
|
|
227
|
+
raise LoginCancelled()
|
|
228
|
+
if ch == b"\x1b": # Esc, or the start of an arrow escape sequence
|
|
229
|
+
r, _, _ = _sel.select([self._fd], [], [], 0.0008)
|
|
230
|
+
if r:
|
|
231
|
+
os.read(self._fd, 2) # swallow the arrow/sequence — don't cancel mid-type
|
|
232
|
+
continue
|
|
233
|
+
raise LoginCancelled()
|
|
234
|
+
if ch in (b"\x7f", b"\x08"): # backspace / delete
|
|
235
|
+
buf = buf[:-1]
|
|
236
|
+
self._draw(active_value=buf, active_slots=slots, helper=helper, error=None)
|
|
237
|
+
continue
|
|
238
|
+
try:
|
|
239
|
+
c = ch.decode("utf-8")
|
|
240
|
+
except UnicodeDecodeError:
|
|
241
|
+
continue
|
|
242
|
+
if not c.isprintable():
|
|
243
|
+
continue
|
|
244
|
+
if slots and (not c.isdigit() or len(buf) >= slots):
|
|
245
|
+
continue # the code field is digits-only, capped
|
|
246
|
+
buf += c
|
|
247
|
+
self._draw(active_value=buf, active_slots=slots, helper=helper, error=None)
|
|
248
|
+
|
|
249
|
+
def text_step(self, label: str, *, helper=None, slots=None, validate=None,
|
|
250
|
+
error_msg=None, hidden_value: bool = False, collapse: bool = True,
|
|
251
|
+
initial_error=None) -> str:
|
|
252
|
+
"""Run one bright ``❯ {label}`` input step; on a failed ``validate`` show the error sub-line
|
|
253
|
+
and re-prompt. On accept it collapses to a dim ``✓ {label} {value}`` line (the value is
|
|
254
|
+
omitted when ``hidden_value``). With ``collapse=False`` it returns the value WITHOUT adding
|
|
255
|
+
the ✓ line (the caller verifies it over the network first, then calls ``note``); ``initial_error``
|
|
256
|
+
seeds the error sub-line on the first draw (a re-prompt after a wrong code)."""
|
|
257
|
+
self.active = label
|
|
258
|
+
error = initial_error
|
|
259
|
+
while True:
|
|
260
|
+
buf = self._read_line(helper=helper, slots=slots, error=error).strip()
|
|
261
|
+
if validate is None or validate(buf):
|
|
262
|
+
self.active = None
|
|
263
|
+
if collapse:
|
|
264
|
+
self.done.append((label, "" if hidden_value else buf))
|
|
265
|
+
self._draw()
|
|
266
|
+
return buf
|
|
267
|
+
error = error_msg or "that doesn't look right"
|
|
268
|
+
|
|
269
|
+
def note(self, label: str, value=None) -> None:
|
|
270
|
+
"""Add a completed ✓ line that wasn't an input step (e.g. ``✓ code sent``)."""
|
|
271
|
+
self.done.append((label, value))
|
|
272
|
+
self._draw()
|
|
273
|
+
|
|
274
|
+
def working(self, text: str) -> None:
|
|
275
|
+
"""Show a transient dim ``· {text}`` line (e.g. while a network call runs)."""
|
|
276
|
+
self._draw(note=text)
|
|
277
|
+
|
|
278
|
+
def retry_text(self, message: str) -> None:
|
|
279
|
+
"""Re-arm the code step with an error sub-line (used after a wrong code)."""
|
|
280
|
+
# text_step's own loop shows the error; this is for the verify-fail re-prompt path.
|
|
281
|
+
self._draw(error=message)
|
|
282
|
+
|
|
283
|
+
def pick(self, slugs: Sequence[str], *, default: Optional[str] = None) -> str:
|
|
284
|
+
"""The nested org-picker inset: arrow keys move the cursor, Enter selects (collapses to
|
|
285
|
+
``✓ org {slug}``), Esc / Ctrl-C raise ``LoginCancelled``."""
|
|
286
|
+
slugs = list(slugs)
|
|
287
|
+
idx = slugs.index(default) if default in slugs else 0
|
|
288
|
+
self.active = None
|
|
289
|
+
self._draw(inset=output.login_inset(slugs, idx))
|
|
290
|
+
while True:
|
|
291
|
+
try:
|
|
292
|
+
key = _read_key(self._fd)
|
|
293
|
+
except KeyboardInterrupt:
|
|
294
|
+
raise LoginCancelled()
|
|
295
|
+
if key in ("UP", "k"):
|
|
296
|
+
idx = (idx - 1) % len(slugs)
|
|
297
|
+
elif key in ("DOWN", "j"):
|
|
298
|
+
idx = (idx + 1) % len(slugs)
|
|
299
|
+
elif key == "ENTER":
|
|
300
|
+
chosen = slugs[idx]
|
|
301
|
+
self.done.append(("org", chosen))
|
|
302
|
+
self._draw()
|
|
303
|
+
return chosen
|
|
304
|
+
elif key in ("ESC", "CTRL_C", "q", "EOF"):
|
|
305
|
+
raise LoginCancelled()
|
|
306
|
+
else:
|
|
307
|
+
continue
|
|
308
|
+
self._draw(inset=output.login_inset(slugs, idx))
|
|
309
|
+
|
|
310
|
+
def finish(self, email: str, org: Optional[str]) -> None:
|
|
311
|
+
"""Final state: the outer border + legend flip SUCCESS green; ``● signed in`` + email + org."""
|
|
312
|
+
self._draw(signed_in=(email, org))
|
|
313
|
+
|
|
314
|
+
def cancel(self, persisted: bool) -> None:
|
|
315
|
+
"""Render the calm close — ``○ cancelled — not signed in`` (or ``○ signed in · pick an org …``
|
|
316
|
+
when the session was already persisted)."""
|
|
317
|
+
self._draw(cancelled=bool(persisted))
|
|
318
|
+
|
|
319
|
+
def fail(self, message: str, hint: Optional[str] = None) -> None:
|
|
320
|
+
"""Render a failure INSIDE the box (red border + ``✗ {message}`` + an optional hint) — e.g.
|
|
321
|
+
a wrong/expired code — instead of a separate error box below the frame."""
|
|
322
|
+
self._draw(failed=(message, hint))
|
fp_cli/theme.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Shared brand color tokens + the permission action→color map.
|
|
2
|
+
|
|
3
|
+
Truecolor hex; Rich downgrades to the nearest supported color automatically and drops color
|
|
4
|
+
entirely under ``NO_COLOR`` / non-color terminals.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
ACCENT = "#9d7bff" # brand mark, box borders, active org, command names in hints
|
|
10
|
+
TEXT = "#d8d2dd" # primary values
|
|
11
|
+
TEXT_DIM = "#7d7488" # ids, resource names, org names
|
|
12
|
+
LABEL = "#6b6478" # field labels, header cells, hints
|
|
13
|
+
FAINT = "#5a5266" # separators, inactive markers
|
|
14
|
+
THIN_RULE = "#2e2435" # the faint rule beneath a list-panel header
|
|
15
|
+
BAR_EMPTY = "#2a2530" # the unfilled cells of a mini-bar (e.g. the aggregate avg bar)
|
|
16
|
+
INSET_BG = "#231e2d" # a hair-lighter fill for a nested inset box (the login org picker)
|
|
17
|
+
|
|
18
|
+
# Errors theme — the one per-command border deviation (the `errors` list + aggregate card).
|
|
19
|
+
# Always red, regardless of error count, so the two views read as one consistent family.
|
|
20
|
+
BORDER_ERROR = "#e2564a" # red → the errors panel border (same as ERROR; consistent, not count-dependent)
|
|
21
|
+
TITLE_ERROR_DIM = "#6e3530" # dim red → the errors title's non-name part + card separators
|
|
22
|
+
RULE_ERROR = "#3a2d2d" # dim red → the errors list header rule (vs the neutral THIN_RULE)
|
|
23
|
+
|
|
24
|
+
# Semantic value colors — for run/job states and score thresholds. (They coincide with
|
|
25
|
+
# the perm risk colors below; named separately so the two uses can diverge later.)
|
|
26
|
+
SUCCESS = "#5dcaa5" # green → run status: done/passed
|
|
27
|
+
AMBER = "#ef9f27" # amber → run status: running/pending · score band .50–.80
|
|
28
|
+
ERROR = "#e2564a" # red → run status: failed/error · score band < .50
|
|
29
|
+
SCORE_HIGH = "#3ddbb8" # cyan-green → score band ≥ .80 (distinct from status green)
|
|
30
|
+
BLUE = "#6b86d8" # blue → schema uuid/timestamp type category
|
|
31
|
+
PINK = "#d4537e" # pink → numeric values (query run cells / scalar card) + numeric type category
|
|
32
|
+
|
|
33
|
+
# Permission verb colors — by ACTION (the part after `:`), never by resource.
|
|
34
|
+
PERM_READ = "#5dcaa5" # green → read
|
|
35
|
+
PERM_WRITE = "#d4537e" # pink → add, create, write, update
|
|
36
|
+
PERM_ACTION = "#ef9f27" # amber → use, trigger, run, ack
|
|
37
|
+
PERM_DANGER = "#e2564a" # red → delete, disable, regenerate
|
|
38
|
+
|
|
39
|
+
PERM_COLORS = {
|
|
40
|
+
"read": PERM_READ,
|
|
41
|
+
"add": PERM_WRITE, "create": PERM_WRITE, "write": PERM_WRITE, "update": PERM_WRITE,
|
|
42
|
+
"use": PERM_ACTION, "trigger": PERM_ACTION, "run": PERM_ACTION, "ack": PERM_ACTION,
|
|
43
|
+
"delete": PERM_DANGER, "disable": PERM_DANGER, "regenerate": PERM_DANGER,
|
|
44
|
+
}
|
|
45
|
+
DEFAULT_PERM_COLOR = LABEL # any unmapped action → neutral dim (never crash / guess a risk)
|
|
46
|
+
|
|
47
|
+
# Risk rank for ordering actions within a row: read → modify → invoke → destroy.
|
|
48
|
+
PERM_RANK = {PERM_READ: 0, PERM_WRITE: 1, PERM_ACTION: 2, PERM_DANGER: 3}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def perm_color(action: str) -> str:
|
|
52
|
+
"""The color for a permission action, or the neutral default if it's unmapped."""
|
|
53
|
+
return PERM_COLORS.get(action, DEFAULT_PERM_COLOR)
|