keywright 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
keywright/config.py ADDED
@@ -0,0 +1,105 @@
1
+ """Where the client finds its server and token.
2
+
3
+ `~/.config/keywright/config.toml` (XDG-aware), directory 0700 and file 0600, holds named
4
+ profiles. Environment wins over the file: KW_URL and KW_TOKEN describe an ad-hoc profile,
5
+ KW_PROFILE picks a named one. Written by hand (three keys) so the client needs no TOML writer.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import tomllib
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+
16
+
17
+ class ConfigError(Exception):
18
+ pass
19
+
20
+
21
+ def config_dir() -> Path:
22
+ base = os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config")
23
+ return Path(base) / "keywright"
24
+
25
+
26
+ def config_file() -> Path:
27
+ return config_dir() / "config.toml"
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class Profile:
32
+ name: str
33
+ url: str
34
+ token: str
35
+
36
+ @property
37
+ def mcp_url(self) -> str:
38
+ url = self.url.rstrip("/")
39
+ return url if url.endswith("/mcp") else url + "/mcp"
40
+
41
+
42
+ def read_all() -> dict[str, dict[str, str]]:
43
+ path = config_file()
44
+ if not path.exists():
45
+ return {}
46
+ try:
47
+ data = tomllib.loads(path.read_text())
48
+ except (tomllib.TOMLDecodeError, OSError) as e:
49
+ raise ConfigError(f"{path}: {e}") from e
50
+ profiles = data.get("profiles") or {}
51
+ return {k: v for k, v in profiles.items() if isinstance(v, dict)}
52
+
53
+
54
+ def load(name: str | None = None) -> Profile:
55
+ env_url, env_token = os.environ.get("KW_URL"), os.environ.get("KW_TOKEN")
56
+ if env_url and env_token:
57
+ return Profile("env", env_url, env_token)
58
+ name = name or os.environ.get("KW_PROFILE") or "default"
59
+ profiles = read_all()
60
+ p = profiles.get(name)
61
+ if not p or not p.get("url") or not p.get("token"):
62
+ raise ConfigError(f"no profile {name!r} in {config_file()}; run `kw login <url> --paste` "
63
+ "or set KW_URL and KW_TOKEN")
64
+ return Profile(name, str(env_url or p["url"]), str(env_token or p["token"]))
65
+
66
+
67
+ def _write(profiles: dict[str, dict[str, str]]) -> None:
68
+ d = config_dir()
69
+ d.mkdir(parents=True, exist_ok=True)
70
+ os.chmod(d, 0o700)
71
+ lines = []
72
+ for name, p in profiles.items():
73
+ lines.append(f"[profiles.{json.dumps(name)}]" if not name.isidentifier() else f"[profiles.{name}]")
74
+ for k in ("url", "token"):
75
+ lines.append(f"{k} = {json.dumps(p[k])}")
76
+ lines.append("")
77
+ path = config_file()
78
+ tmp = path.with_suffix(".tmp")
79
+ tmp.write_text("\n".join(lines))
80
+ os.chmod(tmp, 0o600)
81
+ tmp.replace(path)
82
+ os.chmod(path, 0o600)
83
+
84
+
85
+ def save(name: str, url: str, token: str) -> Path:
86
+ profiles = read_all()
87
+ profiles[name] = {"url": url.strip(), "token": token.strip()}
88
+ _write(profiles)
89
+ return config_file()
90
+
91
+
92
+ def remove(name: str) -> bool:
93
+ profiles = read_all()
94
+ if name not in profiles:
95
+ return False
96
+ del profiles[name]
97
+ _write(profiles)
98
+ return True
99
+
100
+
101
+ def file_mode_ok() -> bool | None:
102
+ path = config_file()
103
+ if not path.exists():
104
+ return None
105
+ return (path.stat().st_mode & 0o077) == 0