databricks-mason 0.1.0.dev0__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.
- databricks_mason/__init__.py +1 -0
- databricks_mason/auth.py +83 -0
- databricks_mason/cli.py +72 -0
- databricks_mason/client.py +298 -0
- databricks_mason/deploy.py +325 -0
- databricks_mason/errors.py +53 -0
- databricks_mason/memory.py +347 -0
- databricks_mason/render.py +185 -0
- databricks_mason/sessions.py +442 -0
- databricks_mason/timefmt.py +85 -0
- databricks_mason/tracing.py +263 -0
- databricks_mason-0.1.0.dev0.dist-info/METADATA +80 -0
- databricks_mason-0.1.0.dev0.dist-info/RECORD +15 -0
- databricks_mason-0.1.0.dev0.dist-info/WHEEL +4 -0
- databricks_mason-0.1.0.dev0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""`mason memory` — manage workspace-scoped managed memory stores and entries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from databricks_mason import render, timefmt
|
|
10
|
+
from databricks_mason.render import field
|
|
11
|
+
|
|
12
|
+
_BREADCRUMB = "Agent Memory"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _store_id(store: dict) -> str:
|
|
16
|
+
name = field(store, "name") or ""
|
|
17
|
+
return name.split("/")[-1] if name else "—"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _truncate(value: Any, length: int = 60) -> str:
|
|
21
|
+
text = "" if value is None else str(value)
|
|
22
|
+
return text if len(text) <= length else text[: length - 1] + "…"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# --- group ------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@click.group()
|
|
29
|
+
def memory() -> None:
|
|
30
|
+
"""Manage agent memory stores and entries (/api/agents/v1/memory-stores)."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@memory.group()
|
|
34
|
+
def stores() -> None:
|
|
35
|
+
"""Workspace-scoped managed memory stores."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@memory.group()
|
|
39
|
+
def entries() -> None:
|
|
40
|
+
"""Memory entries within a store, partitioned by actor."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# --- stores -----------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _store_starter_code(obj, store: dict) -> list[tuple[str, str, str]]:
|
|
47
|
+
store_id = _store_id(store)
|
|
48
|
+
name = field(store, "name") or f"memory-stores/{store_id}"
|
|
49
|
+
return [
|
|
50
|
+
(
|
|
51
|
+
"curl",
|
|
52
|
+
"bash",
|
|
53
|
+
f"""
|
|
54
|
+
curl -X POST "{obj.client().host}/api/agents/v1/{name}/entries" \\
|
|
55
|
+
-H "Authorization: Bearer $DATABRICKS_TOKEN" -H "Content-Type: application/json" \\
|
|
56
|
+
-d '{{"actor_id": "alice", "path": "/preferences/style.md", "content": "Terse, code first."}}'
|
|
57
|
+
""",
|
|
58
|
+
),
|
|
59
|
+
(
|
|
60
|
+
"mason",
|
|
61
|
+
"bash",
|
|
62
|
+
f"""
|
|
63
|
+
mason memory entries create --store {store_id} \\
|
|
64
|
+
--actor-id alice --path /preferences/style.md --content "Terse, code first."
|
|
65
|
+
mason memory entries search --store {store_id} --actor-id alice --query "style"
|
|
66
|
+
""",
|
|
67
|
+
),
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _render_store_detail(obj, store: dict) -> None:
|
|
72
|
+
render.detail(
|
|
73
|
+
_BREADCRUMB,
|
|
74
|
+
field(store, "display_name") or _store_id(store),
|
|
75
|
+
{
|
|
76
|
+
"Name": field(store, "name"),
|
|
77
|
+
"Store ID": _store_id(store),
|
|
78
|
+
"Workspace": field(store, "workspace_id"),
|
|
79
|
+
"Owner": field(store, "owner_user_id"),
|
|
80
|
+
"Storage": render.field(field(store, "storage_backend") or {}, "backend_id"),
|
|
81
|
+
"Description": field(store, "description"),
|
|
82
|
+
"Created": timefmt.absolute(field(store, "created_at")),
|
|
83
|
+
"Updated": timefmt.absolute(field(store, "updated_at")),
|
|
84
|
+
},
|
|
85
|
+
status="ACTIVE",
|
|
86
|
+
snippets=_store_starter_code(obj, store),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@stores.command("create")
|
|
91
|
+
@click.option("--display-name", required=True, help="Workspace-unique display name.")
|
|
92
|
+
@click.option("--description", default=None)
|
|
93
|
+
@click.pass_obj
|
|
94
|
+
def stores_create(obj, display_name, description) -> None:
|
|
95
|
+
"""Create a memory store."""
|
|
96
|
+
data = obj.client().create_memory_store(display_name, description)
|
|
97
|
+
if obj.output == "json":
|
|
98
|
+
render.emit_json(data)
|
|
99
|
+
return
|
|
100
|
+
store_id = _store_id(data)
|
|
101
|
+
render.success(
|
|
102
|
+
f"Created memory store '{display_name}'",
|
|
103
|
+
fields={"Store ID": store_id, "Name": field(data, "name")},
|
|
104
|
+
next_steps=[
|
|
105
|
+
f"mason memory entries create --store {store_id} --actor-id <id> --path </p>",
|
|
106
|
+
f"mason memory stores get {store_id}",
|
|
107
|
+
],
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@stores.command("list")
|
|
112
|
+
@click.option("--page-size", type=int, default=None)
|
|
113
|
+
@click.option("--page-token", default=None)
|
|
114
|
+
@click.pass_obj
|
|
115
|
+
def stores_list(obj, page_size, page_token) -> None:
|
|
116
|
+
"""List memory stores in the workspace."""
|
|
117
|
+
data = obj.client().list_memory_stores(page_size, page_token)
|
|
118
|
+
if obj.output == "json":
|
|
119
|
+
render.emit_json(data)
|
|
120
|
+
return
|
|
121
|
+
items = field(data, "managed_memory_stores") or []
|
|
122
|
+
rows = [
|
|
123
|
+
[
|
|
124
|
+
field(s, "display_name"),
|
|
125
|
+
_store_id(s),
|
|
126
|
+
timefmt.relative(field(s, "created_at")),
|
|
127
|
+
timefmt.relative(field(s, "updated_at")),
|
|
128
|
+
_truncate(field(s, "description"), 40),
|
|
129
|
+
]
|
|
130
|
+
for s in items
|
|
131
|
+
]
|
|
132
|
+
render.resource_table(
|
|
133
|
+
"Managed Memory Stores",
|
|
134
|
+
[
|
|
135
|
+
("Name", "left"),
|
|
136
|
+
("Store ID", "left"),
|
|
137
|
+
("Created", "left"),
|
|
138
|
+
("Updated", "left"),
|
|
139
|
+
("Description", "left"),
|
|
140
|
+
],
|
|
141
|
+
rows,
|
|
142
|
+
subtitle=_page_note(data),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@stores.command("get")
|
|
147
|
+
@click.argument("name")
|
|
148
|
+
@click.pass_obj
|
|
149
|
+
def stores_get(obj, name) -> None:
|
|
150
|
+
"""Get a memory store by id or resource name."""
|
|
151
|
+
data = obj.client().get_memory_store(name)
|
|
152
|
+
if obj.output == "json":
|
|
153
|
+
render.emit_json(data)
|
|
154
|
+
return
|
|
155
|
+
_render_store_detail(obj, data)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@stores.command("update")
|
|
159
|
+
@click.argument("name")
|
|
160
|
+
@click.option("--display-name", default=None)
|
|
161
|
+
@click.option("--description", default=None)
|
|
162
|
+
@click.pass_obj
|
|
163
|
+
def stores_update(obj, name, display_name, description) -> None:
|
|
164
|
+
"""Update a store's display name and/or description."""
|
|
165
|
+
data = obj.client().update_memory_store(name, display_name, description)
|
|
166
|
+
if obj.output == "json":
|
|
167
|
+
render.emit_json(data)
|
|
168
|
+
return
|
|
169
|
+
_render_store_detail(obj, data)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@stores.command("delete")
|
|
173
|
+
@click.argument("name")
|
|
174
|
+
@click.pass_obj
|
|
175
|
+
def stores_delete(obj, name) -> None:
|
|
176
|
+
"""Delete (soft-delete) a memory store."""
|
|
177
|
+
obj.client().delete_memory_store(name)
|
|
178
|
+
if obj.output == "json":
|
|
179
|
+
render.emit_json({"deleted": name})
|
|
180
|
+
return
|
|
181
|
+
render.success(f"Deleted memory store '{name}'")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# --- entries ----------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _render_entry_detail(entry: dict) -> None:
|
|
188
|
+
render.detail(
|
|
189
|
+
f"{_BREADCRUMB} Entry",
|
|
190
|
+
field(entry, "path") or "—",
|
|
191
|
+
{
|
|
192
|
+
"Name": field(entry, "name"),
|
|
193
|
+
"Actor": field(entry, "actor_id"),
|
|
194
|
+
"Session": field(entry, "session_id"),
|
|
195
|
+
"Path": field(entry, "path"),
|
|
196
|
+
"Source": field(entry, "source_type"),
|
|
197
|
+
"Description": field(entry, "description"),
|
|
198
|
+
"Content": field(entry, "content"),
|
|
199
|
+
"Created": timefmt.absolute(field(entry, "create_time")),
|
|
200
|
+
"Updated": timefmt.absolute(field(entry, "update_time")),
|
|
201
|
+
},
|
|
202
|
+
status="ACTIVE",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@entries.command("create")
|
|
207
|
+
@click.option("--store", required=True, help="Store id or resource name.")
|
|
208
|
+
@click.option("--actor-id", required=True)
|
|
209
|
+
@click.option("--path", required=True, help="Absolute path, e.g. /preferences/style.md.")
|
|
210
|
+
@click.option("--content", default=None)
|
|
211
|
+
@click.option("--description", default=None)
|
|
212
|
+
@click.option("--session-id", default=None)
|
|
213
|
+
@click.option(
|
|
214
|
+
"--source-type",
|
|
215
|
+
default=None,
|
|
216
|
+
type=click.Choice(
|
|
217
|
+
["MANAGED_MEMORY_ENTRY_SOURCE_TYPE_AGENT", "MANAGED_MEMORY_ENTRY_SOURCE_TYPE_UNSPECIFIED"]
|
|
218
|
+
),
|
|
219
|
+
)
|
|
220
|
+
@click.pass_obj
|
|
221
|
+
def entries_create(
|
|
222
|
+
obj, store, actor_id, path, content, description, session_id, source_type
|
|
223
|
+
) -> None:
|
|
224
|
+
"""Create a memory entry."""
|
|
225
|
+
data = obj.client().create_memory_entry(
|
|
226
|
+
store, actor_id, path, content, description, session_id, source_type
|
|
227
|
+
)
|
|
228
|
+
if obj.output == "json":
|
|
229
|
+
render.emit_json(data)
|
|
230
|
+
return
|
|
231
|
+
render.success(f"Created memory entry '{path}'", fields={"Name": field(data, "name")})
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@entries.command("get")
|
|
235
|
+
@click.option("--store", required=True)
|
|
236
|
+
@click.argument("entry")
|
|
237
|
+
@click.pass_obj
|
|
238
|
+
def entries_get(obj, store, entry) -> None:
|
|
239
|
+
"""Get an entry by id or resource name (includes content)."""
|
|
240
|
+
data = obj.client().get_memory_entry(store, entry)
|
|
241
|
+
if obj.output == "json":
|
|
242
|
+
render.emit_json(data)
|
|
243
|
+
return
|
|
244
|
+
_render_entry_detail(data)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@entries.command("list")
|
|
248
|
+
@click.option("--store", required=True)
|
|
249
|
+
@click.option("--actor-id", required=True, help="Required partition key.")
|
|
250
|
+
@click.option("--path-prefix", default=None)
|
|
251
|
+
@click.option("--session-id", default=None)
|
|
252
|
+
@click.option("--page-size", type=int, default=None)
|
|
253
|
+
@click.option("--page-token", default=None)
|
|
254
|
+
@click.pass_obj
|
|
255
|
+
def entries_list(obj, store, actor_id, path_prefix, session_id, page_size, page_token) -> None:
|
|
256
|
+
"""List entries for an actor (content omitted)."""
|
|
257
|
+
data = obj.client().list_memory_entries(
|
|
258
|
+
store, actor_id, path_prefix, session_id, page_size, page_token
|
|
259
|
+
)
|
|
260
|
+
if obj.output == "json":
|
|
261
|
+
render.emit_json(data)
|
|
262
|
+
return
|
|
263
|
+
items = field(data, "managed_memory_entries") or []
|
|
264
|
+
rows = [
|
|
265
|
+
[
|
|
266
|
+
field(e, "path"),
|
|
267
|
+
field(e, "actor_id"),
|
|
268
|
+
field(e, "session_id"),
|
|
269
|
+
_truncate(field(e, "description"), 40),
|
|
270
|
+
timefmt.relative(field(e, "update_time")),
|
|
271
|
+
]
|
|
272
|
+
for e in items
|
|
273
|
+
]
|
|
274
|
+
render.resource_table(
|
|
275
|
+
f"Memory Entries · actor {actor_id}",
|
|
276
|
+
[
|
|
277
|
+
("Path", "left"),
|
|
278
|
+
("Actor", "left"),
|
|
279
|
+
("Session", "left"),
|
|
280
|
+
("Description", "left"),
|
|
281
|
+
("Updated", "left"),
|
|
282
|
+
],
|
|
283
|
+
rows,
|
|
284
|
+
subtitle=_page_note(data),
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@entries.command("search")
|
|
289
|
+
@click.option("--store", required=True)
|
|
290
|
+
@click.option("--actor-id", required=True)
|
|
291
|
+
@click.option("--query", required=True)
|
|
292
|
+
@click.option("--limit", type=int, default=None)
|
|
293
|
+
@click.pass_obj
|
|
294
|
+
def entries_search(obj, store, actor_id, query, limit) -> None:
|
|
295
|
+
"""Full-text search an actor's entries, ranked (includes content)."""
|
|
296
|
+
data = obj.client().search_memory_entries(store, actor_id, query, limit)
|
|
297
|
+
if obj.output == "json":
|
|
298
|
+
render.emit_json(data)
|
|
299
|
+
return
|
|
300
|
+
items = field(data, "managed_memory_entries") or []
|
|
301
|
+
rows = [
|
|
302
|
+
[
|
|
303
|
+
field(e, "path"),
|
|
304
|
+
field(e, "actor_id"),
|
|
305
|
+
_truncate(field(e, "content"), 50),
|
|
306
|
+
timefmt.relative(field(e, "update_time")),
|
|
307
|
+
]
|
|
308
|
+
for e in items
|
|
309
|
+
]
|
|
310
|
+
render.resource_table(
|
|
311
|
+
f"Memory Search · '{query}'",
|
|
312
|
+
[("Path", "left"), ("Actor", "left"), ("Content", "left"), ("Updated", "left")],
|
|
313
|
+
rows,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@entries.command("update")
|
|
318
|
+
@click.option("--store", required=True)
|
|
319
|
+
@click.argument("entry")
|
|
320
|
+
@click.option("--content", default=None)
|
|
321
|
+
@click.option("--description", default=None)
|
|
322
|
+
@click.pass_obj
|
|
323
|
+
def entries_update(obj, store, entry, content, description) -> None:
|
|
324
|
+
"""Update an entry's content and/or description."""
|
|
325
|
+
data = obj.client().update_memory_entry(store, entry, content, description)
|
|
326
|
+
if obj.output == "json":
|
|
327
|
+
render.emit_json(data)
|
|
328
|
+
return
|
|
329
|
+
_render_entry_detail(data)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@entries.command("delete")
|
|
333
|
+
@click.option("--store", required=True)
|
|
334
|
+
@click.argument("entry")
|
|
335
|
+
@click.pass_obj
|
|
336
|
+
def entries_delete(obj, store, entry) -> None:
|
|
337
|
+
"""Delete a memory entry."""
|
|
338
|
+
obj.client().delete_memory_entry(store, entry)
|
|
339
|
+
if obj.output == "json":
|
|
340
|
+
render.emit_json({"deleted": entry})
|
|
341
|
+
return
|
|
342
|
+
render.success(f"Deleted memory entry '{entry}'")
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _page_note(data: dict) -> str | None:
|
|
346
|
+
token = field(data, "next_page_token")
|
|
347
|
+
return f"More results available — pass --page-token {token}" if token else None
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Terminal presentation layer for the Mason CLI.
|
|
2
|
+
|
|
3
|
+
Rendering is centralized so every `list`/`get`/`create` command looks consistent.
|
|
4
|
+
Functions accept an optional `console` for testability; default is stdout.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from typing import Any, Iterable, Literal, Optional, Sequence
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
from rich import box
|
|
14
|
+
from rich.console import Console, Group, RenderableType
|
|
15
|
+
from rich.panel import Panel
|
|
16
|
+
from rich.syntax import Syntax
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
from rich.text import Text
|
|
19
|
+
|
|
20
|
+
# Databricks brand accent.
|
|
21
|
+
ACCENT = "#FF3621"
|
|
22
|
+
MUTED = "grey62"
|
|
23
|
+
|
|
24
|
+
_stdout = Console()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def console() -> Console:
|
|
28
|
+
return _stdout
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# --- small helpers -----------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def field(obj: dict, name: str) -> Any:
|
|
35
|
+
"""Read a field tolerating snake_case or camelCase JSON keys."""
|
|
36
|
+
if name in obj:
|
|
37
|
+
return obj[name]
|
|
38
|
+
parts = name.split("_")
|
|
39
|
+
camel = parts[0] + "".join(p.title() for p in parts[1:])
|
|
40
|
+
return obj.get(camel)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def emit_json(data: Any) -> None:
|
|
44
|
+
"""Print raw JSON for `--output json` (stable, pipe-friendly)."""
|
|
45
|
+
click.echo(json.dumps(data, indent=2, default=str))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def status_pill(status: Optional[str]) -> Text:
|
|
49
|
+
"""Create a colored ●/○ status indicator."""
|
|
50
|
+
value = (status or "").strip().upper()
|
|
51
|
+
if value in {"ACTIVE", "RUNNING", "READY"}:
|
|
52
|
+
return Text("● ", style="green") + Text(value.title(), style="green")
|
|
53
|
+
if value in {"PENDING", "CREATING", "STARTING", "DEPLOYING"}:
|
|
54
|
+
return Text("○ ", style="yellow") + Text(value.title(), style="yellow")
|
|
55
|
+
if value in {"DISABLED", "DELETED", "STOPPED", "ERROR", "FAILED"}:
|
|
56
|
+
return Text("⨯ ", style="red") + Text(value.title(), style="red")
|
|
57
|
+
return Text("● ", style=MUTED) + Text(value.title() or "Unknown", style=MUTED)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# --- list view ---------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def resource_table(
|
|
64
|
+
title: str,
|
|
65
|
+
columns: Sequence[tuple[str, Literal["default", "left", "center", "right", "full"]]],
|
|
66
|
+
rows: Iterable[Sequence[Any]],
|
|
67
|
+
*,
|
|
68
|
+
subtitle: Optional[str] = None,
|
|
69
|
+
con: Optional[Console] = None,
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Render a titled list table.
|
|
72
|
+
|
|
73
|
+
`columns` is a sequence of (header, justify) where justify is left/right/center.
|
|
74
|
+
"""
|
|
75
|
+
con = con or _stdout
|
|
76
|
+
rows = list(rows)
|
|
77
|
+
|
|
78
|
+
con.print()
|
|
79
|
+
con.print(Text(title, style=f"bold {ACCENT}"))
|
|
80
|
+
if subtitle:
|
|
81
|
+
con.print(Text(subtitle, style=MUTED))
|
|
82
|
+
|
|
83
|
+
table = Table(box=box.SIMPLE_HEAD, expand=False, pad_edge=False, show_edge=False)
|
|
84
|
+
for header, justify in columns:
|
|
85
|
+
table.add_column(header.upper(), justify=justify, header_style=f"bold {MUTED}")
|
|
86
|
+
for row in rows:
|
|
87
|
+
table.add_row(*[_cell(v) for v in row])
|
|
88
|
+
con.print(table)
|
|
89
|
+
|
|
90
|
+
con.print(Text(f"{len(rows)} item{'s' if len(rows) != 1 else ''}", style=MUTED))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _cell(value: Any) -> Any:
|
|
94
|
+
if isinstance(value, Text):
|
|
95
|
+
return value
|
|
96
|
+
if value is None:
|
|
97
|
+
return Text("—", style=MUTED)
|
|
98
|
+
return str(value)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# --- detail view (aig-endpoint.png) ------------------------------------------
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def detail(
|
|
105
|
+
breadcrumb: str,
|
|
106
|
+
name: str,
|
|
107
|
+
fields: dict[str, Any],
|
|
108
|
+
*,
|
|
109
|
+
status: Optional[str] = None,
|
|
110
|
+
snippets: Optional[Sequence[tuple[str, str, str]]] = None,
|
|
111
|
+
con: Optional[Console] = None,
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Render a resource detail page.
|
|
114
|
+
|
|
115
|
+
`breadcrumb` is the section label ("Agent Memory"); `fields` is an ordered
|
|
116
|
+
key -> value map for the details rail. `snippets` is a sequence of
|
|
117
|
+
(label, lexer, code) blocks shown under a "Starter code" panel.
|
|
118
|
+
"""
|
|
119
|
+
con = con or _stdout
|
|
120
|
+
|
|
121
|
+
con.print()
|
|
122
|
+
con.print(Text(f"{breadcrumb} › ", style=MUTED) + Text(name, style="bold"))
|
|
123
|
+
if status is not None:
|
|
124
|
+
con.print(status_pill(status))
|
|
125
|
+
con.print()
|
|
126
|
+
|
|
127
|
+
grid = Table.grid(padding=(0, 3))
|
|
128
|
+
grid.add_column(style=MUTED, justify="left")
|
|
129
|
+
grid.add_column(justify="left")
|
|
130
|
+
for key, value in fields.items():
|
|
131
|
+
grid.add_row(key, _cell(value))
|
|
132
|
+
con.print(grid)
|
|
133
|
+
|
|
134
|
+
if snippets:
|
|
135
|
+
con.print()
|
|
136
|
+
con.print(
|
|
137
|
+
Panel(
|
|
138
|
+
_snippet_group(snippets),
|
|
139
|
+
title="Starter code",
|
|
140
|
+
title_align="left",
|
|
141
|
+
border_style=MUTED,
|
|
142
|
+
box=box.ROUNDED,
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _snippet_group(snippets: Sequence[tuple[str, str, str]]) -> RenderableType:
|
|
148
|
+
parts: list[RenderableType] = []
|
|
149
|
+
for i, (label, lexer, code) in enumerate(snippets):
|
|
150
|
+
if i:
|
|
151
|
+
parts.append(Text())
|
|
152
|
+
parts.append(Text(label, style=f"bold {MUTED}"))
|
|
153
|
+
parts.append(Syntax(code.strip(), lexer, background_color="default", word_wrap=True))
|
|
154
|
+
return Group(*parts)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# --- success / next steps (the "Set up" cards) -------------------------------
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def success(
|
|
161
|
+
title: str,
|
|
162
|
+
*,
|
|
163
|
+
fields: Optional[dict[str, Any]] = None,
|
|
164
|
+
next_steps: Optional[Sequence[str]] = None,
|
|
165
|
+
con: Optional[Console] = None,
|
|
166
|
+
) -> None:
|
|
167
|
+
"""A green success panel with optional details and a Next steps list."""
|
|
168
|
+
con = con or _stdout
|
|
169
|
+
body: list[RenderableType] = [Text("✓ ", style="green") + Text(title, style="bold")]
|
|
170
|
+
|
|
171
|
+
if fields:
|
|
172
|
+
grid = Table.grid(padding=(0, 3))
|
|
173
|
+
grid.add_column(style=MUTED)
|
|
174
|
+
grid.add_column()
|
|
175
|
+
for key, value in fields.items():
|
|
176
|
+
grid.add_row(key, _cell(value))
|
|
177
|
+
body.append(grid)
|
|
178
|
+
|
|
179
|
+
if next_steps:
|
|
180
|
+
body.append(Text("Next steps", style=f"bold {MUTED}"))
|
|
181
|
+
for step in next_steps:
|
|
182
|
+
body.append(Text(" → ", style=ACCENT) + Text(step))
|
|
183
|
+
|
|
184
|
+
con.print()
|
|
185
|
+
con.print(Panel(Group(*body), border_style="green", box=box.ROUNDED))
|