"""UI pages for attribute sets: list, inspect (with what they resolve to and who reuses them), create and edit.""" from __future__ import annotations import html from pathlib import Path from . import user as USER from .attrs import fold, save_set, set_layers, used_by EXTRA_ROWS = 3 def e(s) -> str: return html.escape(str(s)) def pill(text: str) -> str: return f"{e(text)}" def _is_app_level(doc) -> bool: return Path(doc.path) == USER.user_file() def _where(app, doc) -> str: if _is_app_level(doc): return "app (you)" for r in app.roots: try: return f"{r.name}/{Path(doc.path).relative_to(r)}" except ValueError: continue return str(doc.path) def _usage(app, reg, sid: str) -> str: u = used_by(reg, sid) parts = [] parts += [f"{e(p)}" for p in u["projects"]] parts += [f"{e(t)} (template)" for t in u["templates"]] parts += [f"{e(x)} (extends it)" for x in u["sets"]] return ", ".join(parts) or "nothing yet" def sets_page(app, msg: str = "") -> bytes: from .ui import page reg = app.registry() rows = "" for sid, doc in sorted(reg.sets.items()): n = len(doc.data.get("attributes", {})) if doc.ok else 0 state = "" if doc.ok else f" invalid: {e(doc.errors[0])}" ext = ", ".join(f"{e(x)}" for x in doc.data.get("extends", [])) or "-" rows += (f"{e(doc.data.get('title', sid))}{state}
{e(sid)}" f"{e(_where(app, doc))}{ext}{n}{_usage(app, reg, sid)}") roots = "".join(f"" for i, r in enumerate(app.roots)) extends_help = ", ".join(sorted(reg.sets)) or "none yet" body = ("

Attribute sets

" "

Named, reusable, non-secret facts. Runbooks receive them as inputs, projects and templates compose them, and one set " "can extend another (later layers win, and every value keeps its source). Nothing here may be a secret.

" + (f"

{e(msg)}

" if msg else "") + ("" + rows + "
SetWhere it livesExtendsAttributesReused by
" if rows else "

No attribute sets yet.

") + "
New attribute set" f"
{app.hidden()}" "

" "

" f"

" f"

" "
") return page("Attribute sets", body) def set_page(app, sid: str, msg: str = "") -> bytes | None: from .ui import page reg = app.registry() doc = reg.sets.get(sid) if doc is None: return None d = doc.data out = [f"

{e(d.get('title', sid))}

{pill('app' if _is_app_level(doc) else 'repository')} {e(sid)} " f"{e(_where(app, doc))}

"] if msg: out.append(f"

{e(msg)}

") if not doc.ok: out.append("

This set is invalid:

") return page(d.get("title", sid), "".join(out)) out.append(f"

Reused by: {_usage(app, reg, sid)}

") try: folded = fold(set_layers(reg, sid)) except ValueError as ex: out.append(f"

{e(ex)}

") folded = {} rows = "" for name, a in folded.items(): val = "" if a["value"] is None else a["value"] rows += (f"{e(name)}{e(val) if val != '' else '(no value)'}" f"{e(a['requirement'] or '-')}{e(a.get('note', ''))}{e(a['source'] or '-')}" f"{' ' + pill('modified') if a['modified'] else ''}") out.append("

What this set resolves to

Including everything it extends; the source shows which set gave each value.

" "" + rows + "
AttributeValueRequirementNoteSource
") if _is_app_level(doc): out.append("

This is your set: it follows you across every project. Change it on " "your settings page. A repository may define its own set with the same id, which then wins.

") return page(d.get("title", sid), "".join(out)) own = d.get("attributes", {}) trs = "" from .attrs import normalize for i, (name, a) in enumerate(normalize(own).items()): req = a.get("requirement") or "" opts = "".join(f"" for o in ("", "required", "optional")) trs += (f"" f"" f"") base = len(own) for j in range(EXTRA_ROWS): i = base + j trs += (f"" f"" f"") warn = "" u = used_by(reg, sid) if u["projects"] or u["templates"] or u["sets"]: warn = ("

Other things reuse this set. Changing a value changes what their runbooks receive from now on; " "runs that already started keep the values they started with.

") out.append("

Edit

" + warn + f"
{app.hidden()}" f"

" f"

" f"" "" + trs + "
AttributeValueRequirementNote
" "

A value can be left empty for a required attribute: it then has to be given by whoever uses the set. " "Never enter secrets.

") return page(d.get("title", sid), "".join(out)) def _split(text: str) -> list[str]: return [x for x in text.replace(",", " ").split() if x] def handle_post(app, parts: list[str], form: dict) -> str: """Create or edit an attribute set. Returns the URL to go to; raises ValueError with the reason if refused.""" reg = app.registry() if parts == ["attribute-sets", "new"]: try: root = app.roots[int(form.get("root", "0"))] except (ValueError, IndexError): raise ValueError("choose a repository") sid = form.get("set_id", "").strip() if sid in reg.sets: raise ValueError(f"an attribute set {sid} already exists") save_set(reg, root, sid, form.get("title", ""), _split(form.get("extends", "")), {}) return f"/attribute-set/{sid}" if len(parts) == 2 and parts[0] == "attribute-set": sid = parts[1] doc = reg.sets.get(sid) if doc is None: raise KeyError(sid) if _is_app_level(doc): raise ValueError("this is your app-level set: change it on your settings page") attrs: dict = {} for i in range(int(form.get("rows", "0") or 0)): name = form.get(f"n{i}", "").strip() if not name or form.get(f"x{i}"): continue if name in attrs: raise ValueError(f"{name} appears twice") attrs[name] = {"value": form.get(f"v{i}", ""), "requirement": form.get(f"r{i}", ""), "note": form.get(f"t{i}", "")} save_set(reg, Path(doc.path).parent.parent, sid, form.get("title", ""), _split(form.get("extends", "")), attrs, existing_path=Path(doc.path)) return f"/attribute-set/{sid}" raise ValueError("unknown action")