runbook-tutorials/rtut/user.py
tegwick 8d62c1fbe8
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 3s
Work Records / validate (push) Has been cancelled
Active user attribute set, latest activities, guarded changes to used values, artefact links, durations and averages; irreversible showcase step
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: sonnet
Assistant-Process: 295952@bnt-lap001
Assistant-Session: e93f64ad-516c-46eb-9666-aad8d300c477
2026-09-29 15:50:56 +02:00

89 lines
3.5 KiB
Python

"""The active user: who is running things. Lives in the app, outside any project, as the attribute set `user-attributes`.
Order of derivation: the RTUT_USER environment variable (for a wrapper that knows the login), the saved
`user-attributes.yaml` in the app's config folder, then the operating-system login. If the UI later sits behind a
login, that login is what should fill the same attribute; forms already read only from here.
"""
from __future__ import annotations
import getpass
import os
import re
from pathlib import Path
import yaml
from .attrs import Doc
from .pack import SECRET_RE
USER_SET_ID = "user-attributes"
NAME = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,39}")
def config_dir() -> Path:
return Path(os.environ.get("RTUT_CONFIG_DIR") or (Path.home() / ".config" / "runbook-tutorials"))
def user_file() -> Path:
return config_dir() / "user-attributes.yaml"
def _one_word(text: str) -> str:
word = re.sub(r"[^A-Za-z0-9._-]+", "-", text.strip()).strip("-._")
return word[:40] or "operator"
def _saved() -> dict:
f = user_file()
if not f.exists():
return {}
try:
data = yaml.safe_load(f.read_text()) or {}
except yaml.YAMLError:
return {}
out = {}
for k, v in (data.get("attributes") or {}).items():
out[k] = v.get("value") if isinstance(v, dict) else v
return {k: str(v) for k, v in out.items() if v is not None}
def current() -> dict:
"""{'operator': one word, 'display_name': str, 'source': 'environment'|'saved'|'login'}"""
saved = _saved()
env = os.environ.get("RTUT_USER", "").strip()
if env:
return {"operator": _one_word(env), "display_name": saved.get("display_name", ""), "source": "environment"}
if saved.get("operator") and NAME.fullmatch(saved["operator"]):
return {"operator": saved["operator"], "display_name": saved.get("display_name", ""), "source": "saved"}
try:
login = getpass.getuser()
except Exception:
login = "operator"
return {"operator": _one_word(login), "display_name": saved.get("display_name", ""), "source": "login"}
def save(operator: str, display_name: str = "") -> Path:
operator = operator.strip()
if not NAME.fullmatch(operator):
raise ValueError("the operator name is one word: letters, digits, dot, dash or underscore (max 40)")
if SECRET_RE.search(operator) or SECRET_RE.search(display_name):
raise ValueError("names are never secret")
attrs: dict = {"operator": {"value": operator, "requirement": "required",
"note": "One word that identifies you in runs, reviews and activity."}}
if display_name.strip():
attrs["display_name"] = {"value": display_name.strip()[:80], "requirement": "optional"}
f = user_file()
f.parent.mkdir(parents=True, exist_ok=True)
f.write_text(yaml.safe_dump({"spec": "attribute-set/v0.1", "id": USER_SET_ID, "title": "User attributes",
"attributes": attrs}, sort_keys=False))
return f
def attribute_set() -> Doc:
"""The active user as an attribute set, for projects and forms to reuse (`attribute_sets: [user-attributes]`)."""
u = current()
attrs = {"operator": {"value": u["operator"], "requirement": "required"}}
if u["display_name"]:
attrs["display_name"] = {"value": u["display_name"], "requirement": "optional"}
return Doc(user_file(), {"spec": "attribute-set/v0.1", "id": USER_SET_ID, "title": "User attributes",
"attributes": attrs}, [])