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
This commit is contained in:
parent
6716cee921
commit
8d62c1fbe8
22 changed files with 1025 additions and 35 deletions
89
rtut/user.py
Normal file
89
rtut/user.py
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
"""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}, [])
|
||||
Loading…
Add table
Add a link
Reference in a new issue