"""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}, [])