dashui
DashUI — shared ipywidgets component library for the Dashlibs suite. Import from here in any dash-* package instead of duplicating widget code.
1""" 2DashUI — shared ipywidgets component library for the Dashlibs suite. 3Import from here in any dash-* package instead of duplicating widget code. 4""" 5from dashui.components import ( 6 EditableTable, 7 EnvSetupPanel, 8 SourceSelector, 9 action_button, 10 card, 11 editable_table, 12 env_setup_panel, 13 header, 14 html, 15 output_panel, 16 running_list, 17 section, 18 source_selector, 19 status_line, 20) 21from dashui.persistence import ( 22 clear_config_dir, 23 config_path, 24 get_config_dir, 25 load_config, 26 save_config, 27 set_config_dir, 28) 29from dashui.schema import list_columns, list_columns_safe 30from dashui.theme import ( 31 ACCENT_BG, 32 ACCENT_FG, 33 BORDER, 34 BORDER_STRONG, 35 CARD, 36 DANGER, 37 FONT_MONO, 38 FONT_SANS, 39 INFO, 40 MUTED, 41 PRIMARY, 42 SUCCESS, 43 WARNING, 44 accent, 45) 46 47__version__ = "0.3.5" 48__all__ = [ 49 "SourceSelector", 50 "EditableTable", 51 "EnvSetupPanel", 52 "action_button", 53 "card", 54 "editable_table", 55 "env_setup_panel", 56 "header", 57 "html", 58 "output_panel", 59 "running_list", 60 "section", 61 "source_selector", 62 "status_line", 63 "list_columns", 64 "list_columns_safe", 65 "get_config_dir", 66 "set_config_dir", 67 "clear_config_dir", 68 "config_path", 69 "load_config", 70 "save_config", 71 "accent", 72 "PRIMARY", 73 "SUCCESS", 74 "DANGER", 75 "WARNING", 76 "INFO", 77 "BORDER", 78 "BORDER_STRONG", 79 "CARD", 80 "MUTED", 81 "ACCENT_BG", 82 "ACCENT_FG", 83 "FONT_SANS", 84 "FONT_MONO", 85]
259@dataclass 260class SourceSelector: 261 """ 262 The UC Table / DataFrame variable / SQL Query source picker. 263 264 Usage:: 265 src = source_selector() 266 ui = card([src.toggle, src.box, ...]) 267 kind, value = src.value() 268 """ 269 toggle: object # visible segmented-control container (place in card layout) 270 box: object # visible input-area container (place in card layout) 271 table_input: object # hidden Text — readable/settable for testing 272 df_input: object # hidden Text 273 sql_input: object # hidden Textarea 274 _mode_widget: object = field(default=None, repr=False) # hidden Text tracking selected mode 275 276 def value(self) -> tuple[str, str]: 277 """Returns (kind, value) where kind is 'table' | 'dataframe' | 'sql'.""" 278 mode = self._mode_widget.value if self._mode_widget else "UC Table" 279 if mode == "UC Table": 280 return "table", self.table_input.value.strip() 281 if mode == "DataFrame variable": 282 return "dataframe", self.df_input.value.strip() 283 return "sql", self.sql_input.value.strip() 284 285 def resolve_df(self): 286 """Resolve the selected source to a Spark DataFrame.""" 287 kind, value = self.value() 288 if kind == "dataframe": 289 import IPython 290 shell = IPython.get_ipython() 291 df = shell.user_ns.get(value) if shell else None 292 if df is None: 293 raise ValueError(f"Variable '{value}' not found") 294 return df 295 from pyspark.sql import SparkSession 296 spark = SparkSession.getActiveSession() 297 if kind == "table": 298 return spark.table(value) 299 return spark.sql(value)
The UC Table / DataFrame variable / SQL Query source picker.
Usage:: src = source_selector() ui = card([src.toggle, src.box, ...]) kind, value = src.value()
276 def value(self) -> tuple[str, str]: 277 """Returns (kind, value) where kind is 'table' | 'dataframe' | 'sql'.""" 278 mode = self._mode_widget.value if self._mode_widget else "UC Table" 279 if mode == "UC Table": 280 return "table", self.table_input.value.strip() 281 if mode == "DataFrame variable": 282 return "dataframe", self.df_input.value.strip() 283 return "sql", self.sql_input.value.strip()
Returns (kind, value) where kind is 'table' | 'dataframe' | 'sql'.
285 def resolve_df(self): 286 """Resolve the selected source to a Spark DataFrame.""" 287 kind, value = self.value() 288 if kind == "dataframe": 289 import IPython 290 shell = IPython.get_ipython() 291 df = shell.user_ns.get(value) if shell else None 292 if df is None: 293 raise ValueError(f"Variable '{value}' not found") 294 return df 295 from pyspark.sql import SparkSession 296 spark = SparkSession.getActiveSession() 297 if kind == "table": 298 return spark.table(value) 299 return spark.sql(value)
Resolve the selected source to a Spark DataFrame.
431@dataclass 432class EditableTable: 433 """ 434 Add/remove-row key-value grid. Row data is batch-synced to a hidden 435 Textarea as JSON; values() parses it — no per-cell widget needed. 436 437 Usage:: 438 tbl = editable_table(["Key", "Value"], placeholders={"Key": "AWS_REGION"}) 439 ui = card([tbl.widget, ...]) 440 rows = tbl.values() # [{"Key": "AWS_REGION", "Value": "us-east-1"}, ...] 441 """ 442 widget: object 443 add_row: object # Python-side no-op; rows are added by the JS "+ Add row" button 444 values: object # callable() -> list[dict[str, str]]
Add/remove-row key-value grid. Row data is batch-synced to a hidden Textarea as JSON; values() parses it — no per-cell widget needed.
Usage:: tbl = editable_table(["Key", "Value"], placeholders={"Key": "AWS_REGION"}) ui = card([tbl.widget, ...]) rows = tbl.values() # [{"Key": "AWS_REGION", "Value": "us-east-1"}, ...]
206def card(children, padding: str = "16px"): 207 """Bordered, shadowed VBox — the outer shell for every launch() UI.""" 208 w = _require_widgets() 209 global _STYLE_INJECTED 210 body = [_global_style(), *children] if not _STYLE_INJECTED else list(children) 211 _STYLE_INJECTED = True 212 box = w.VBox(body, layout=w.Layout(padding=padding)) 213 box.add_class("dashui-card") 214 box.add_class("dashui-root") 215 return box
Bordered, shadowed VBox — the outer shell for every launch() UI.
447def editable_table( 448 columns: list[str], 449 placeholders: dict[str, str] | None = None, 450 initial_rows: int = 1, 451) -> EditableTable: 452 w = _require_widgets() 453 placeholders = placeholders or {} 454 455 # Hidden Textarea — JS serialises all rows to JSON here on every change 456 hidden_ta = w.Textarea(value="[]") 457 hidden_ta.add_class("dashui-hw") 458 uid = _uid(hidden_ta) 459 hidden_ta.add_class(f"dh{uid}") 460 461 # Header cells 462 th_s = ( 463 f"text-align:left;font-size:11px;font-weight:600;text-transform:uppercase;" 464 f"letter-spacing:.03em;color:{MUTED_FOREGROUND};background:{MUTED};" 465 f"padding:6px 8px;border-bottom:1px solid {BORDER};" 466 ) 467 headers = "".join(f"<th style='{th_s}'>{c}</th>" for c in columns) 468 headers += f"<th style='{th_s}width:32px;'></th>" 469 470 cols_j = _json.dumps(columns) 471 phs_j = _json.dumps(placeholders) 472 473 table_html = w.HTML( 474 f'<div style="border:1px solid {BORDER};border-radius:{RADIUS_MD};overflow:hidden;margin-bottom:6px">' 475 f'<table style="width:100%;border-collapse:collapse" id="du-et-{uid}">' 476 f'<thead><tr>{headers}</tr></thead>' 477 f'<tbody id="du-et-body-{uid}"></tbody>' 478 f'</table>' 479 f'</div>' 480 f'<button style="border:1px solid {BORDER_STRONG};border-radius:{RADIUS_SM};' 481 f'font-size:{FONT_SIZE_BASE};font-family:{FONT_SANS};background:{CARD};' 482 f'color:#1B3139;padding:4px 12px;height:28px;cursor:pointer;"' 483 f' onclick="window[\'duEtAdd_{uid}\']&&window[\'duEtAdd_{uid}\']()">+ Add row</button>' 484 f'<script>' 485 f'(function(){{' 486 f' var uid=\'{uid}\',COLS={cols_j},PHS={phs_j};' 487 f' function sync(){{' 488 f' var rows=[];' 489 f' document.querySelectorAll("#du-et-body-"+uid+" tr").forEach(function(tr){{' 490 f' var row={{}};' 491 f' tr.querySelectorAll("input[data-col]").forEach(function(inp){{row[inp.dataset.col]=inp.value;}});' 492 f' rows.push(row);' 493 f' }});' 494 f' var ta=document.querySelector(".dh"+uid+" textarea");' 495 f' if(ta){{ta.value=JSON.stringify(rows);ta.dispatchEvent(new Event("input",{{bubbles:true}}))}}' 496 f' }}' 497 f' function addRow(){{' 498 f' var tbody=document.getElementById("du-et-body-"+uid);' 499 f' if(!tbody)return;' 500 f' var tr=document.createElement("tr");' 501 f' tr.style.borderBottom="1px solid {BORDER}";' 502 f' COLS.forEach(function(col){{' 503 f' var td=document.createElement("td");td.style.padding="3px 4px";' 504 f' var inp=document.createElement("input");' 505 f' inp.type="text";inp.placeholder=PHS[col]||"";inp.dataset.col=col;' 506 f' inp.style.border="1px solid {BORDER_STRONG}";' 507 f' inp.style.borderRadius="{RADIUS_SM}";' 508 f' inp.style.fontSize="{FONT_SIZE_BASE}";' 509 f' inp.style.fontFamily="Inter,-apple-system,sans-serif";' 510 f' inp.style.padding="3px 6px";' 511 f' inp.style.background="{CARD}";' 512 f' inp.style.color="#1B3139";' 513 f' inp.style.width="100%";' 514 f' inp.style.boxSizing="border-box";' 515 f' inp.addEventListener("input",sync);' 516 f' td.appendChild(inp);tr.appendChild(td);' 517 f' }});' 518 f' var tdR=document.createElement("td");tdR.style.padding="3px 4px";tdR.style.width="32px";' 519 f' var rmv=document.createElement("button");' 520 f' rmv.textContent="✕";' 521 f' rmv.style.border="1px solid {BORDER_STRONG}";' 522 f' rmv.style.borderRadius="{RADIUS_SM}";' 523 f' rmv.style.fontSize="11px";' 524 f' rmv.style.background="{CARD}";' 525 f' rmv.style.color="#5A6872";' 526 f' rmv.style.cursor="pointer";' 527 f' rmv.style.width="28px";rmv.style.height="26px";' 528 f' rmv.style.padding="0";rmv.style.lineHeight="1";' 529 f' rmv.addEventListener("click",function(){{tr.remove();sync();}});' 530 f' tdR.appendChild(rmv);tr.appendChild(tdR);tbody.appendChild(tr);sync();' 531 f' }}' 532 f' window["duEtAdd_{uid}"]=addRow;' 533 f' for(var i=0;i<{initial_rows};i++)addRow();' 534 f'}})();' 535 f'</script>' 536 ) 537 538 container = w.VBox([table_html, hidden_ta]) 539 540 def values() -> list[dict]: 541 try: 542 data = _json.loads(hidden_ta.value or "[]") 543 return [ 544 row for row in data 545 if any(str(v).strip() for v in row.values()) 546 ] 547 except Exception: 548 return [] 549 550 return EditableTable(widget=container, add_row=lambda _b=None: None, values=values)
555def env_setup_panel(library: str, extra_fields: dict | None = None): 556 """ 557 Ready-to-embed "Environment Setup" panel. 558 559 `extra_fields` is {label: placeholder} for library-specific settings 560 saved alongside the config directory. All settings are returned by 561 the `values()` callable on the returned EnvSetupPanel. 562 """ 563 from dashui.persistence import get_config_dir, load_config, save_config, set_config_dir 564 565 w = _require_widgets() 566 extra_fields = extra_fields or {} 567 saved = load_config(library, name="env") 568 569 dir_input = w.Text( 570 description="Config directory:", 571 value=saved.get("config_dir", get_config_dir(library)), 572 placeholder=get_config_dir(library), 573 layout=w.Layout(width="420px"), 574 ) 575 extra_inputs = { 576 label: w.Text( 577 description=f"{label}:", value=saved.get(label, ""), placeholder=placeholder, 578 ) 579 for label, placeholder in extra_fields.items() 580 } 581 582 save_btn = action_button("Save", style="primary") 583 reload_btn = action_button("Reload", style="info") 584 status = html( 585 f"<span style='font-size:12px;color:{MUTED_FOREGROUND}'>Currently using: " 586 f"<code>{get_config_dir(library)}</code></span>" 587 ) 588 589 def _collect() -> dict: 590 return { 591 "config_dir": dir_input.value.strip() or get_config_dir(library), 592 **{label: field.value for label, field in extra_inputs.items()}, 593 } 594 595 def _on_save(_b): 596 config = _collect() 597 set_config_dir(library, config["config_dir"]) 598 path = save_config(library, config, name="env") 599 status.value = ( 600 f"<span style='font-size:12px;color:{SUCCESS}'>Saved — settings will be read from " 601 f"<code>{path}</code> in future sessions.</span>" 602 ) 603 604 def _on_reload(_b): 605 current = load_config(library, name="env") 606 dir_input.value = current.get("config_dir", get_config_dir(library)) 607 for label, inp in extra_inputs.items(): 608 inp.value = current.get(label, "") 609 status.value = ( 610 f"<span style='font-size:12px;color:{MUTED_FOREGROUND}'>Reloaded from " 611 f"<code>{config_path_display(library)}</code>.</span>" 612 ) 613 614 save_btn.on_click(_on_save) 615 reload_btn.on_click(_on_reload) 616 617 panel = w.VBox([ 618 html( 619 f"<div style='font-size:12px;color:{MUTED_FOREGROUND};margin-bottom:4px'>" 620 "Where should this package's configs be read/written? Leave as-is to use " 621 "the notebook's current working directory — nothing here is required." 622 "</div>" 623 ), 624 dir_input, 625 *extra_inputs.values(), 626 w.HBox([save_btn, reload_btn]), 627 status, 628 ]) 629 return EnvSetupPanel(widget=panel, values=_collect)
Ready-to-embed "Environment Setup" panel.
extra_fields is {label: placeholder} for library-specific settings
saved alongside the config directory. All settings are returned by
the values() callable on the returned EnvSetupPanel.
173def header(title: str, library: str = "default", emoji: str = "", subtitle: str = ""): 174 """Top banner. `emoji` kept for API compatibility — not rendered (plain text, no glyph).""" 175 color = accent(library) 176 prefix = f"{emoji} " if emoji else "" 177 sub = ( 178 f"<div style='font-size:12px;color:{MUTED_FOREGROUND};margin-top:2px;" 179 f"font-family:{FONT_SANS}'>{subtitle}</div>" 180 ) if subtitle else "" 181 return html( 182 f"<h2 style='color:{color};margin-bottom:0;font-weight:700;" 183 f"letter-spacing:-0.01em;font-family:{FONT_SANS}'>{prefix}{title}</h2>{sub}" 184 )
Top banner. emoji kept for API compatibility — not rendered (plain text, no glyph).
400def output_panel(): 401 """Standard scrollable output area for run/profile results.""" 402 w = _require_widgets() 403 out = w.Output(layout=w.Layout(padding="12px")) 404 out.add_class("dashui-output") 405 return out
Standard scrollable output area for run/profile results.
408def running_list(formatter): 409 """ 410 Live-updating numbered list — pattern for 'added entities' accumulators. 411 412 Usage:: 413 out, render = running_list(lambda i, item: f"{i}. {item['name']}") 414 render(items) # call again to refresh 415 """ 416 w = _require_widgets() 417 out = w.Output(layout=w.Layout(padding="8px 12px")) 418 out.add_class("dashui-output") 419 420 def render(items: list): 421 with out: 422 out.clear_output() 423 for i, item in enumerate(items, 1): 424 print(formatter(i, item)) 425 426 return out, render
Live-updating numbered list — pattern for 'added entities' accumulators.
Usage:: out, render = running_list(lambda i, item: f"{i}. {item['name']}") render(items) # call again to refresh
187def section(title: str): 188 """Step/section divider.""" 189 return html(f"<div class='dashui-section'>{title}</div>")
Step/section divider.
302def source_selector(label: str = "Source:") -> SourceSelector: 303 w = _require_widgets() 304 305 # Hidden data-channel widgets — JS syncs user input into these 306 hidden_mode = w.Text(value="UC Table") 307 hidden_table = w.Text(placeholder="catalog.schema.table") 308 hidden_df = w.Text(placeholder="df") 309 hidden_sql = w.Textarea(placeholder="SELECT * FROM ...") 310 311 mid = _uid(hidden_mode) 312 tid = _uid(hidden_table) 313 did = _uid(hidden_df) 314 sid = _uid(hidden_sql) 315 316 for hw, uid in [(hidden_mode, mid), (hidden_table, tid), 317 (hidden_df, did), (hidden_sql, sid)]: 318 hw.add_class("dashui-hw") 319 hw.add_class(f"dh{uid}") 320 321 # ── Segmented control ────────────────────────────────────────────────── 322 base_s = ( 323 f"border:1px solid {BORDER_STRONG};background:{CARD};color:#1B3139;" 324 f"font-size:{FONT_SIZE_BASE};font-family:{FONT_SANS};" 325 f"padding:5px 12px;cursor:pointer;transition:background .12s;" 326 ) 327 act_s = f"background:{ACCENT_BG};border-color:{PRIMARY};color:{ACCENT_FG};font-weight:600;" 328 329 toggle_html = w.HTML( 330 f'<div style="margin-bottom:4px">' 331 f'<button id="du-uc-{mid}" style="{base_s}border-radius:{RADIUS_SM} 0 0 {RADIUS_SM};{act_s}"' 332 f' onclick="duSrcSel(\'{mid}\',\'{tid}\',\'{did}\',\'{sid}\',\'UC Table\',this)">' 333 f'UC Table</button>' 334 f'<button id="du-df-{mid}" style="{base_s}border-radius:0;border-left:none;"' 335 f' onclick="duSrcSel(\'{mid}\',\'{tid}\',\'{did}\',\'{sid}\',\'DataFrame variable\',this)">' 336 f'DataFrame variable</button>' 337 f'<button id="du-sql-{mid}" style="{base_s}border-radius:0 {RADIUS_SM} {RADIUS_SM} 0;border-left:none;"' 338 f' onclick="duSrcSel(\'{mid}\',\'{tid}\',\'{did}\',\'{sid}\',\'SQL Query\',this)">' 339 f'SQL Query</button>' 340 f'</div>' 341 f'<script>' 342 # duSrcSel and duSrcSync are shared helpers — defined once via the || idiom 343 f'window.duSrcSel=window.duSrcSel||function(mid,tid,did,sid,mode,btn){{' 344 f' [["uc","{CARD}","{BORDER_STRONG}","#1B3139","500"],' 345 f' ["df","{CARD}","{BORDER_STRONG}","#1B3139","500"],' 346 f' ["sql","{CARD}","{BORDER_STRONG}","#1B3139","500"]' 347 f' ].forEach(function(x){{var b=document.getElementById("du-"+x[0]+"-"+mid);' 348 f' if(b){{b.style.background=x[1];b.style.borderColor=x[2];' 349 f' b.style.color=x[3];b.style.fontWeight=x[4];}}}});' 350 f' btn.style.background="{ACCENT_BG}";btn.style.borderColor="{PRIMARY}";' 351 f' btn.style.color="{ACCENT_FG}";btn.style.fontWeight="600";' 352 f' document.getElementById("du-uc-panel-"+mid).style.display=mode==="UC Table"?"":"none";' 353 f' document.getElementById("du-df-panel-"+mid).style.display=mode==="DataFrame variable"?"":"none";' 354 f' document.getElementById("du-sql-panel-"+mid).style.display=mode==="SQL Query"?"":"none";' 355 f' var m=document.querySelector(".dh"+mid+" input");' 356 f' if(m){{m.value=mode;m.dispatchEvent(new Event("input",{{bubbles:true}}))}}' 357 f'}};' 358 f'window.duSrcSync=window.duSrcSync||function(cls,tag,val){{' 359 f' var el=document.querySelector(cls+" "+tag);' 360 f' if(el){{el.value=val;el.dispatchEvent(new Event("input",{{bubbles:true}}))}}' 361 f'}};' 362 f'</script>' 363 ) 364 365 # ── Input panels ─────────────────────────────────────────────────────── 366 inp_s = ( 367 f"width:100%;border:1px solid {BORDER_STRONG};border-radius:{RADIUS_SM};" 368 f"font-size:{FONT_SIZE_BASE};font-family:{FONT_SANS};padding:4px 8px;" 369 f"box-sizing:border-box;background:{CARD};color:#1B3139;" 370 ) 371 372 input_html = w.HTML( 373 f'<div id="du-uc-panel-{mid}">' 374 f'<input type="text" placeholder="catalog.schema.table" style="{inp_s}"' 375 f' oninput="window.duSrcSync&&window.duSrcSync(\'.dh{tid}\',\'input\',this.value)">' 376 f'</div>' 377 f'<div id="du-df-panel-{mid}" style="display:none">' 378 f'<input type="text" placeholder="df" style="{inp_s}"' 379 f' oninput="window.duSrcSync&&window.duSrcSync(\'.dh{did}\',\'input\',this.value)">' 380 f'</div>' 381 f'<div id="du-sql-panel-{mid}" style="display:none">' 382 f'<textarea placeholder="SELECT * FROM ..." rows="3" style="{inp_s}resize:vertical;"' 383 f' oninput="window.duSrcSync&&window.duSrcSync(\'.dh{sid}\',\'textarea\',this.value)">' 384 f'</textarea>' 385 f'</div>' 386 ) 387 388 toggle = w.VBox([toggle_html, hidden_mode]) 389 box = w.VBox([input_html, hidden_table, hidden_df, hidden_sql]) 390 391 return SourceSelector( 392 toggle=toggle, box=box, 393 table_input=hidden_table, df_input=hidden_df, sql_input=hidden_sql, 394 _mode_widget=hidden_mode, 395 )
192def status_line(text: str, kind: str = "info"): 193 """One-line status. kind in success|error|warning|info — color carried by a small dot.""" 194 color = { 195 "success": SUCCESS, "error": DANGER, "warning": WARNING, "info": MUTED_FOREGROUND, 196 }.get(kind, MUTED_FOREGROUND) 197 return html( 198 f"<span style='font-family:{FONT_SANS};color:#1B3139'>" 199 f"<span style='display:inline-block;width:6px;height:6px;border-radius:50%;" 200 f"background:{color};margin-right:7px'></span>{text}</span>" 201 )
One-line status. kind in success|error|warning|info — color carried by a small dot.
6def list_columns(table: str) -> list[str]: 7 """Return column names for a UC table, without loading any data.""" 8 from pyspark.sql import SparkSession 9 spark = SparkSession.getActiveSession() 10 return [f.name for f in spark.table(table).schema.fields]
Return column names for a UC table, without loading any data.
13def list_columns_safe(table: str) -> list[str]: 14 """Like list_columns, but returns [] instead of raising — for UI dropdowns.""" 15 try: 16 return list_columns(table) 17 except Exception: 18 return []
Like list_columns, but returns [] instead of raising — for UI dropdowns.
29def get_config_dir(library: str) -> str: 30 """The directory configs for `library` should be read/written from: 31 the env_setup()-configured directory if one was set, else cwd.""" 32 pointer = _pointer_path(library) 33 if pointer.exists(): 34 try: 35 configured = json.loads(pointer.read_text()).get("config_dir") 36 if configured: 37 return configured 38 except Exception: 39 pass 40 return os.environ.get("DASHLIBS_CONFIG_DIR", os.getcwd())
The directory configs for library should be read/written from:
the env_setup()-configured directory if one was set, else cwd.
43def set_config_dir(library: str, path: str) -> None: 44 """Remember `path` as this library's config directory for future sessions.""" 45 pointer = _pointer_path(library) 46 pointer.parent.mkdir(parents=True, exist_ok=True) 47 pointer.write_text(json.dumps({"config_dir": path}, indent=2))
Remember path as this library's config directory for future sessions.
50def clear_config_dir(library: str) -> None: 51 """Forget the configured directory — future calls fall back to cwd again.""" 52 pointer = _pointer_path(library) 53 if pointer.exists(): 54 pointer.unlink()
Forget the configured directory — future calls fall back to cwd again.
61def load_config(library: str, name: str = "config", defaults: dict | None = None) -> dict: 62 """Read `<config_dir>/<library>_<name>.json`, merged over `defaults`. 63 Returns `defaults` (or {}) unchanged if the file doesn't exist or is invalid.""" 64 path = config_path(library, name) 65 if os.path.exists(path): 66 try: 67 with open(path) as f: 68 return {**(defaults or {}), **json.load(f)} 69 except Exception: 70 pass 71 return dict(defaults or {})
Read <config_dir>/<library>_<name>.json, merged over defaults.
Returns defaults (or {}) unchanged if the file doesn't exist or is invalid.
74def save_config(library: str, config: dict, name: str = "config") -> str: 75 """Write `config` to `<config_dir>/<library>_<name>.json`. Returns the path written.""" 76 path = config_path(library, name) 77 os.makedirs(os.path.dirname(path) or ".", exist_ok=True) 78 with open(path, "w") as f: 79 json.dump(config, f, indent=2) 80 return path
Write config to <config_dir>/<library>_<name>.json. Returns the path written.
72def accent(library: str) -> str: 73 """Look up the accent color for a Dashlibs package name (e.g. 'dashsynthetic').""" 74 return ACCENTS.get(library, ACCENTS["default"])
Look up the accent color for a Dashlibs package name (e.g. 'dashsynthetic').