qso-graph-auth 0.1.0__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.
@@ -0,0 +1,3 @@
1
+ """QSO-Graph Auth — persona and credential management for MCP servers."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,7 @@
1
+ """CLI package for qso-graph-auth."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .root import main
6
+
7
+ __all__ = ["main"]
@@ -0,0 +1,234 @@
1
+ """Credentials subcommands (OS keyring)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import getpass
7
+ import json
8
+ from typing import Optional
9
+
10
+ from qso_graph_auth.cli.provider import auth_type, list_supported
11
+ from qso_graph_auth.credentials import (
12
+ SERVICE,
13
+ Credentials,
14
+ delete_creds,
15
+ get_creds,
16
+ set_creds,
17
+ )
18
+ from qso_graph_auth.identity.store import PersonaStore
19
+
20
+ try:
21
+ import keyring
22
+ except Exception:
23
+ keyring = None # type: ignore[assignment]
24
+
25
+
26
+ def _redacted(c: Credentials) -> dict[str, str]:
27
+ def mask(v: str) -> str:
28
+ return (v[:2] + "…") if len(v) > 4 else "•••"
29
+
30
+ out: dict[str, str] = {}
31
+ if c.username:
32
+ out["username"] = c.username
33
+ if c.password:
34
+ out["password"] = mask(c.password)
35
+ if c.api_key:
36
+ out["api_key"] = mask(c.api_key)
37
+ return out
38
+
39
+
40
+ def cmd_set(args: argparse.Namespace) -> int:
41
+ if keyring is None:
42
+ print("Install keyring: pip install keyring")
43
+ return 2
44
+
45
+ prov = args.provider.lower()
46
+ a = auth_type(prov)
47
+
48
+ username: str | None = args.username
49
+ password: str | None = args.password
50
+ api_key: str | None = args.api_key
51
+
52
+ def _norm(s: str | None) -> str | None:
53
+ if s is None:
54
+ return None
55
+ s = s.strip()
56
+ return s if s else None
57
+
58
+ if a == "username_password":
59
+ username = _norm(username) or input(f"{prov} username/callsign: ").strip() or None
60
+ if password is None:
61
+ pw = getpass.getpass(f"{prov} password: ").strip()
62
+ password = pw or None
63
+ api_key = None
64
+ elif a == "api_key":
65
+ username = _norm(username) or input(f"{prov} username/callsign: ").strip() or None
66
+ api_key = _norm(api_key) or input(f"{prov} API key: ").strip() or None
67
+ password = None
68
+ else:
69
+ username = _norm(username) or input("Username (blank to skip): ").strip() or None
70
+ api_key = _norm(api_key) or input("API key (blank to skip): ").strip() or None
71
+ if password is None:
72
+ pw = getpass.getpass("Password (hidden, blank to skip): ").strip()
73
+ password = pw or None
74
+
75
+ creds = Credentials(
76
+ username=_norm(username), password=_norm(password), api_key=_norm(api_key)
77
+ )
78
+ set_creds(args.persona, prov, creds)
79
+ print(f"Saved credentials for {args.persona}:{prov} in {SERVICE}.")
80
+ return 0
81
+
82
+
83
+ def cmd_get(args: argparse.Namespace) -> int:
84
+ if keyring is None:
85
+ print("Install keyring: pip install keyring")
86
+ return 2
87
+ c = get_creds(args.persona, args.provider)
88
+ if not c:
89
+ print("No credentials stored.")
90
+ return 1
91
+ if getattr(args, "raw", False):
92
+ print(c.to_json())
93
+ else:
94
+ print(json.dumps(_redacted(c), indent=2))
95
+ return 0
96
+
97
+
98
+ def cmd_delete(args: argparse.Namespace) -> int:
99
+ if keyring is None:
100
+ print("Install keyring: pip install keyring")
101
+ return 2
102
+ ok = delete_creds(args.persona, args.provider)
103
+ print("Deleted." if ok else "Nothing to delete.")
104
+ return 0
105
+
106
+
107
+ def cmd_reset(args: argparse.Namespace) -> int:
108
+ if not args.yes:
109
+ print("Refusing to reset without --yes (this deletes ALL personas and credentials)")
110
+ return 2
111
+ if keyring is None:
112
+ print("Install keyring: pip install keyring")
113
+ return 2
114
+
115
+ store = PersonaStore()
116
+ personas = store.list()
117
+ cred_count = 0
118
+
119
+ for p in personas:
120
+ for prov in list(p.providers.keys()):
121
+ if delete_creds(p.name, prov):
122
+ cred_count += 1
123
+
124
+ persona_count = 0
125
+ for p in personas:
126
+ store.remove(p.name)
127
+ persona_count += 1
128
+
129
+ print(f"Reset complete: {persona_count} persona(s), {cred_count} credential(s) deleted.")
130
+ return 0
131
+
132
+
133
+ def cmd_list(args: argparse.Namespace) -> int:
134
+ if keyring is None:
135
+ print("Install keyring: pip install keyring")
136
+ return 2
137
+ print(
138
+ "Keyring entries are stored under service "
139
+ f"'{SERVICE}' with username 'persona:provider'.\n"
140
+ "Use your OS keychain UI to enumerate entries."
141
+ )
142
+ return 0
143
+
144
+
145
+ def _has_required_fields(c: Optional[Credentials], a: str) -> bool:
146
+ a = a.lower()
147
+ if a == "username_password":
148
+ return bool(c and c.username and c.password)
149
+ if a == "api_key":
150
+ return bool(c and c.username and c.api_key)
151
+ return bool(c and (c.username or c.password or c.api_key))
152
+
153
+
154
+ def cmd_doctor(args: argparse.Namespace) -> int:
155
+ store = PersonaStore()
156
+ personas = store.list()
157
+ if not personas:
158
+ print("(no personas) — create one with: qso-auth persona add --name NAME …")
159
+ return 0
160
+
161
+ target = (args.persona or "").lower() if hasattr(args, "persona") else ""
162
+ ok = missing = 0
163
+
164
+ for p in personas:
165
+ if not p.name or (target and p.name.lower() != target):
166
+ continue
167
+
168
+ for prov in list_supported():
169
+ if prov not in p.providers:
170
+ continue
171
+ a = auth_type(prov)
172
+ c = get_creds(p.name, prov)
173
+ if _has_required_fields(c, a):
174
+ print(f" {p.name}:{prov} — stored ({a})")
175
+ ok += 1
176
+ else:
177
+ need = (
178
+ "username+password"
179
+ if a == "username_password"
180
+ else (
181
+ "username+api_key" if a == "api_key" else "some secret"
182
+ )
183
+ )
184
+ print(
185
+ f" {p.name}:{prov} — missing {need} "
186
+ f"(run: qso-auth creds set {p.name} {prov})"
187
+ )
188
+ missing += 1
189
+
190
+ print(f"\nSummary: stored={ok}, missing={missing}")
191
+ return 0
192
+
193
+
194
+ def register_cli(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
195
+ p = subparsers.add_parser(
196
+ "creds",
197
+ help="Manage credentials in the OS keyring.",
198
+ description="Set/get/delete credentials per persona/provider.",
199
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
200
+ )
201
+
202
+ sp: argparse._SubParsersAction[argparse.ArgumentParser] = p.add_subparsers(
203
+ dest="creds_cmd", required=True
204
+ )
205
+
206
+ s = sp.add_parser("set", help="Save credentials.")
207
+ s.add_argument("persona", help="Persona name")
208
+ s.add_argument("provider", help="Provider (eqsl, lotw, qrz, qrz_logbook, hamqth)")
209
+ s.add_argument("--username", default=None, help="Username/login")
210
+ s.add_argument("--password", default=None, help="Password (unsafe on CLI)")
211
+ s.add_argument("--api-key", default=None, help="API key/token")
212
+ s.set_defaults(func=cmd_set)
213
+
214
+ g = sp.add_parser("get", help="Show redacted credentials.")
215
+ g.add_argument("persona", help="Persona name")
216
+ g.add_argument("provider", help="Provider")
217
+ g.add_argument("--raw", action="store_true", help="Print raw JSON (Use with Caution)")
218
+ g.set_defaults(func=cmd_get)
219
+
220
+ d = sp.add_parser("delete", help="Delete stored credentials.")
221
+ d.add_argument("persona", help="Persona name")
222
+ d.add_argument("provider", help="Provider")
223
+ d.set_defaults(func=cmd_delete)
224
+
225
+ list_parser = sp.add_parser("list", help="Explain where entries live.")
226
+ list_parser.set_defaults(func=cmd_list)
227
+
228
+ r = sp.add_parser("reset", help="Delete ALL personas and credentials (factory reset).")
229
+ r.add_argument("--yes", action="store_true", help="Confirm destructive op")
230
+ r.set_defaults(func=cmd_reset)
231
+
232
+ doc = sp.add_parser("doctor", help="Check creds across personas/providers.")
233
+ doc.add_argument("--persona", default=None, help="Only check this persona")
234
+ doc.set_defaults(func=cmd_doctor)
@@ -0,0 +1,204 @@
1
+ """Persona subcommands backed by PersonaStore (JSON index)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from datetime import date
7
+
8
+ from qso_graph_auth.credentials import Credentials, set_creds
9
+ from qso_graph_auth.identity.store import PersonaStore
10
+ from qso_graph_auth.paths import config_dir
11
+
12
+
13
+ def _validate_dates(
14
+ start: str | None, end: str | None,
15
+ ) -> tuple[date | None, date | None]:
16
+ if not start:
17
+ raise SystemExit("--start YYYY-MM-DD is required")
18
+ s = start.strip()
19
+ try:
20
+ ds = date.fromisoformat(s)
21
+ except Exception as exc:
22
+ raise SystemExit(f"invalid --start '{start}'") from exc
23
+ de: date | None = None
24
+ if end:
25
+ e = end.strip()
26
+ try:
27
+ de = date.fromisoformat(e)
28
+ except Exception as exc:
29
+ raise SystemExit(f"invalid --end '{end}'") from exc
30
+ if de < ds:
31
+ raise SystemExit("end date must be >= start date")
32
+ return ds, de
33
+
34
+
35
+ def cmd_list(args: argparse.Namespace) -> int:
36
+ store = PersonaStore()
37
+ personas = store.list()
38
+ if not personas:
39
+ print("(no personas) — create with: qso-auth persona add --name NAME …")
40
+ return 0
41
+
42
+ verbose = bool(getattr(args, "verbose", False))
43
+ for p in personas:
44
+ providers = ", ".join(sorted(p.providers.keys())) or "(none)"
45
+ if verbose:
46
+ print(
47
+ f"{p.name:15s} {p.callsign:10s} "
48
+ f"{p.active_span():23s} providers: {providers}"
49
+ )
50
+ else:
51
+ print(f"{p.name:15s} providers: {providers}")
52
+ return 0
53
+
54
+
55
+ def cmd_add(args: argparse.Namespace) -> int:
56
+ name = args.name.strip().lower()
57
+ if not name:
58
+ print("persona --name is required")
59
+ return 1
60
+
61
+ callsign = (args.callsign or "").strip().upper()
62
+ if not callsign:
63
+ raise SystemExit("callsign is required")
64
+
65
+ ds, de = _validate_dates(args.start, args.end)
66
+
67
+ store = PersonaStore()
68
+ existing = store.get(name)
69
+ if existing and not args.force:
70
+ print(f"Persona {name} already exists. Use --force to overwrite.")
71
+ return 1
72
+
73
+ store.upsert(name=name, callsign=callsign, start=ds, end=de)
74
+
75
+ providers = [str(p).lower() for p in (args.providers or [])]
76
+ for prov in providers:
77
+ store.set_provider_ref(persona=name, provider=prov, username="")
78
+
79
+ print(f"Created persona {name}")
80
+ return 0
81
+
82
+
83
+ def cmd_remove(args: argparse.Namespace) -> int:
84
+ store = PersonaStore()
85
+ if not store.remove(args.name):
86
+ print(f"No persona {args.name}")
87
+ return 1
88
+ print(f"Removed persona {args.name}")
89
+ return 0
90
+
91
+
92
+ def cmd_remove_all(args: argparse.Namespace) -> int:
93
+ if not args.yes:
94
+ print("Refusing to remove all personas without --yes")
95
+ return 2
96
+ store = PersonaStore()
97
+ personas = store.list()
98
+ if not personas:
99
+ print("(no personas)")
100
+ return 0
101
+ count = 0
102
+ for p in personas:
103
+ store.remove(p.name)
104
+ count += 1
105
+ print(f"Removed {count} persona(s)")
106
+ return 0
107
+
108
+
109
+ def cmd_show(args: argparse.Namespace) -> int:
110
+ store = PersonaStore()
111
+ p = store.get(args.name)
112
+ if not p:
113
+ print(f"No persona {args.name} found.")
114
+ return 1
115
+ print(f"name: {p.name}")
116
+ print(f"callsign: {p.callsign}")
117
+ print(f"start: {p.start.isoformat() if p.start else '—'}")
118
+ print(f"end: {p.end.isoformat() if p.end else '—'}")
119
+ if p.providers:
120
+ print(f"providers: {', '.join(sorted(p.providers.keys()))}")
121
+ else:
122
+ print("providers: (none)")
123
+ return 0
124
+
125
+
126
+ def cmd_set_active(args: argparse.Namespace) -> int:
127
+ cfg = config_dir()
128
+ marker = cfg / "current.txt"
129
+ marker.write_text(args.name, encoding="utf-8")
130
+ print(f"Set active persona to {args.name}")
131
+ return 0
132
+
133
+
134
+ def cmd_set_credential(args: argparse.Namespace) -> int:
135
+ name = args.persona
136
+ provider = args.provider.lower()
137
+
138
+ def _norm(s: str | None) -> str | None:
139
+ if s is None:
140
+ return None
141
+ s = s.strip()
142
+ return s if s else None
143
+
144
+ creds = Credentials(
145
+ username=_norm(args.username),
146
+ password=_norm(args.password),
147
+ api_key=_norm(args.api_key),
148
+ )
149
+ set_creds(name, provider, creds)
150
+ print(f"Saved credentials for {name}:{provider}.")
151
+ return 0
152
+
153
+
154
+ def register_cli(
155
+ subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
156
+ ) -> None:
157
+ p = subparsers.add_parser(
158
+ "persona",
159
+ help="Manage personas",
160
+ description="Manage persona profiles with callsign date ranges.",
161
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
162
+ )
163
+ sp = p.add_subparsers(dest="persona_cmd", required=True)
164
+
165
+ s_list = sp.add_parser("list", help="List personas")
166
+ s_list.add_argument("--verbose", action="store_true", help="Show callsign and date range")
167
+ s_list.set_defaults(func=cmd_list)
168
+
169
+ s_add = sp.add_parser("add", help="Add a new persona")
170
+ s_add.add_argument("--name", required=True, help="Persona name")
171
+ s_add.add_argument("--callsign", required=True, help="Primary callsign")
172
+ s_add.add_argument("--start", required=True, help="Start date YYYY-MM-DD (inclusive)")
173
+ s_add.add_argument("--end", help="End date YYYY-MM-DD (inclusive)")
174
+ s_add.add_argument("--providers", nargs="*", help="Initial providers (e.g. eqsl lotw qrz)")
175
+ s_add.add_argument("--force", action="store_true", help="Overwrite if exists")
176
+ s_add.set_defaults(func=cmd_add)
177
+
178
+ s_rm = sp.add_parser("remove", help="Remove a persona")
179
+ s_rm.add_argument("name", help="Persona name")
180
+ s_rm.set_defaults(func=cmd_remove)
181
+
182
+ s_rmall = sp.add_parser("remove-all", help="Remove ALL personas")
183
+ s_rmall.add_argument("--yes", action="store_true", help="Confirm destructive op")
184
+ s_rmall.set_defaults(func=cmd_remove_all)
185
+
186
+ s_show = sp.add_parser("show", help="Show persona config")
187
+ s_show.add_argument("name", help="Persona name")
188
+ s_show.set_defaults(func=cmd_show)
189
+
190
+ s_set = sp.add_parser("set-active", help="Mark persona as active")
191
+ s_set.add_argument("name", help="Persona name")
192
+ s_set.set_defaults(func=cmd_set_active)
193
+
194
+ s_sc = sp.add_parser("set-credential", help="Set credentials for a persona/provider")
195
+ s_sc.add_argument("--persona", required=True, help="Persona name")
196
+ s_sc.add_argument(
197
+ "--provider", required=True,
198
+ choices=["eqsl", "lotw", "qrz", "qrz_logbook", "hamqth"],
199
+ help="Provider slug",
200
+ )
201
+ s_sc.add_argument("--username", help="Username/callsign", default=None)
202
+ s_sc.add_argument("--password", help="Password", default=None)
203
+ s_sc.add_argument("--api-key", help="API key/token", default=None)
204
+ s_sc.set_defaults(func=cmd_set_credential)
@@ -0,0 +1,159 @@
1
+ """Provider subcommands and metadata access (dynamic discovery)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ from functools import cache
8
+ from importlib.resources import files
9
+ from pathlib import Path
10
+ from typing import Any, Dict, List, cast
11
+
12
+ from qso_graph_auth.credentials import get_creds
13
+ from qso_graph_auth.identity.store import PersonaStore
14
+
15
+ # ---------- Provider metadata (resources/providers/*.json) ----------
16
+
17
+ PROVIDERS_DIR: Path = Path(files("qso_graph_auth.resources") / "providers") # type: ignore[arg-type]
18
+
19
+
20
+ def _provider_path(slug: str) -> Path:
21
+ return Path(PROVIDERS_DIR) / f"{slug}.json"
22
+
23
+
24
+ def list_supported() -> List[str]:
25
+ p = Path(PROVIDERS_DIR)
26
+ if not p.exists():
27
+ return []
28
+ return sorted(f.stem for f in p.glob("*.json"))
29
+
30
+
31
+ @cache
32
+ def get_provider(slug: str) -> Dict[str, Any]:
33
+ path = _provider_path(slug)
34
+ try:
35
+ with path.open("r", encoding="utf-8") as fh:
36
+ data = json.load(fh)
37
+ return cast(Dict[str, Any], data or {})
38
+ except FileNotFoundError:
39
+ return {"name": slug, "slug": slug, "auth": "none"}
40
+ except json.JSONDecodeError:
41
+ return {"name": slug, "slug": slug, "auth": "none"}
42
+
43
+
44
+ def auth_type(slug: str) -> str:
45
+ meta = get_provider(slug)
46
+ return str(meta.get("auth", "none")).lower()
47
+
48
+
49
+ def _require_supported(provider: str) -> str:
50
+ p = provider.lower()
51
+ supported = set(list_supported())
52
+ if p not in supported:
53
+ raise SystemExit(
54
+ f"Unknown provider '{provider}'. Supported: {', '.join(sorted(supported))}"
55
+ )
56
+ return p
57
+
58
+
59
+ def cmd_list(args: argparse.Namespace) -> int:
60
+ providers = list_supported()
61
+ print("Supported providers: " + ", ".join(providers or ["(none found)"]))
62
+
63
+ store = PersonaStore()
64
+ personas = store.list()
65
+ if not personas:
66
+ print("(no personas discovered)")
67
+ return 0
68
+
69
+ print("\nEnabled per persona:")
70
+ for p in personas:
71
+ enabled = [k for k in sorted(p.providers.keys()) if k in providers]
72
+ print(f" {p.name:15s} -> {', '.join(enabled) or '(none)'}")
73
+ return 0
74
+
75
+
76
+ def cmd_enable(args: argparse.Namespace) -> int:
77
+ persona = args.persona
78
+ provider = _require_supported(args.provider)
79
+
80
+ store = PersonaStore()
81
+ p = store.get(persona)
82
+ if not p:
83
+ print(f"Persona '{persona}' not found")
84
+ return 1
85
+
86
+ if provider in p.providers:
87
+ print(f"{provider} already enabled for {persona}")
88
+ else:
89
+ store.set_provider_ref(persona=persona, provider=provider, username="")
90
+ print(f"Enabled {provider} for {persona}")
91
+
92
+ c = get_creds(persona, provider)
93
+ a = auth_type(provider)
94
+ need = (
95
+ "username+password"
96
+ if a == "username_password"
97
+ else ("username+api_key" if a == "api_key" else "some secret")
98
+ )
99
+ missing = (
100
+ not c
101
+ or (a == "username_password" and not (c.username and c.password))
102
+ or (a == "api_key" and not (c.username and c.api_key))
103
+ )
104
+ if missing:
105
+ print(
106
+ f"note: no credentials for {persona}:{provider} ({a}). "
107
+ f"Run: qso-auth creds set {persona} {provider} # need {need}"
108
+ )
109
+ return 0
110
+
111
+
112
+ def cmd_disable(args: argparse.Namespace) -> int:
113
+ persona = args.persona
114
+ provider = _require_supported(args.provider)
115
+
116
+ store = PersonaStore()
117
+ p = store.get(persona)
118
+ if not p:
119
+ print(f"Persona '{persona}' not found")
120
+ return 1
121
+
122
+ try:
123
+ removed = store.remove_provider_ref(persona=persona, provider=provider)
124
+ except KeyError:
125
+ print(f"Persona '{persona}' not found")
126
+ return 1
127
+
128
+ if not removed:
129
+ print(f"{provider} already disabled for {persona}")
130
+ else:
131
+ print(f"Disabled {provider} for {persona}")
132
+ return 0
133
+
134
+
135
+ def register_cli(
136
+ subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
137
+ ) -> None:
138
+ p = subparsers.add_parser(
139
+ "provider",
140
+ help="Provider operations",
141
+ description="Enable/disable providers on persona profiles.",
142
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
143
+ )
144
+ sp: argparse._SubParsersAction[argparse.ArgumentParser] = p.add_subparsers(
145
+ dest="provider_cmd", required=True
146
+ )
147
+
148
+ s_list = sp.add_parser("list", help="List supported and enabled providers.")
149
+ s_list.set_defaults(func=cmd_list)
150
+
151
+ s_en = sp.add_parser("enable", help="Enable a provider for a persona.")
152
+ s_en.add_argument("persona", help="Persona name")
153
+ s_en.add_argument("provider", help="Provider to enable")
154
+ s_en.set_defaults(func=cmd_enable)
155
+
156
+ s_dis = sp.add_parser("disable", help="Disable a provider for a persona.")
157
+ s_dis.add_argument("persona", help="Persona name")
158
+ s_dis.add_argument("provider", help="Provider to disable")
159
+ s_dis.set_defaults(func=cmd_disable)
@@ -0,0 +1,52 @@
1
+ """Root CLI wiring for qso-auth."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from typing import Callable, Protocol, cast
8
+
9
+ from . import creds, persona, provider
10
+
11
+
12
+ class _RegisterCLI(Protocol):
13
+ def __call__(self, sp: argparse._SubParsersAction[argparse.ArgumentParser]) -> None: ...
14
+
15
+
16
+ def build_parser() -> argparse.ArgumentParser:
17
+ parser = argparse.ArgumentParser(
18
+ prog="qso-auth",
19
+ description="Persona and credential management for qso-graph MCP servers",
20
+ )
21
+ parser.add_argument(
22
+ "--version", action="version",
23
+ version=f"%(prog)s {__import__('qso_graph_auth').__version__}",
24
+ )
25
+
26
+ subparsers = parser.add_subparsers(dest="command")
27
+
28
+ if hasattr(persona, "register_cli"):
29
+ cast(_RegisterCLI, getattr(persona, "register_cli"))(subparsers)
30
+ if hasattr(provider, "register_cli"):
31
+ cast(_RegisterCLI, getattr(provider, "register_cli"))(subparsers)
32
+ if hasattr(creds, "register_cli"):
33
+ cast(_RegisterCLI, getattr(creds, "register_cli"))(subparsers)
34
+
35
+ return parser
36
+
37
+
38
+ def main(argv: list[str] | None = None) -> int:
39
+ args_in = sys.argv[1:] if argv is None else argv
40
+ parser = build_parser()
41
+
42
+ if not args_in:
43
+ parser.print_help()
44
+ return 0
45
+
46
+ args = parser.parse_args(args_in)
47
+ func = cast(Callable[[argparse.Namespace], int] | None, getattr(args, "func", None))
48
+ if func is not None:
49
+ return func(args)
50
+
51
+ parser.print_help()
52
+ return 2
@@ -0,0 +1,19 @@
1
+ """Credentials package for qso-graph-auth."""
2
+
3
+ from .credentials import (
4
+ SERVICE,
5
+ Credentials,
6
+ delete_creds,
7
+ ensure_keyring,
8
+ get_creds,
9
+ set_creds,
10
+ )
11
+
12
+ __all__ = [
13
+ "SERVICE",
14
+ "Credentials",
15
+ "delete_creds",
16
+ "ensure_keyring",
17
+ "get_creds",
18
+ "set_creds",
19
+ ]