Skill Tracer
Skills › Scientific Agent Skills › labarchive-integration › setup_config.py

setup_config.py

262 lines · skills/labarchive-integration/scripts/setup_config.py at 5f2bc03

1#!/usr/bin/env python3
2"""Validate LabArchives regional endpoints and named environment variables.
3
4This utility never reads .env files, writes configuration, authenticates, or
5prints credential values.
6"""
7
8from __future__ import annotations
9
10import argparse
11import getpass
12import json
13import os
14import sys
15from collections.abc import Mapping, Sequence
16from typing import Any
17from urllib.parse import urlsplit
20REGIONS: dict[str, dict[str, str]] = {
21 "us": {
22 "label": "US and rest of world",
23 "login_url": "https://mynotebook.labarchives.com",
24 "eln_api_url": "https://api.labarchives.com/api",
25 },
26 "ca": {
27 "label": "Canada",
28 "login_url": "https://ca-mynotebook.labarchives.com",
29 "eln_api_url": "https://caapi.labarchives.com/api",
30 },
31 "au": {
32 "label": "Australia and New Zealand",
33 "login_url": "https://au-mynotebook.labarchives.com",
34 "eln_api_url": "https://auapi.labarchives.com/api",
35 },
36 "uk": {
37 "label": "United Kingdom",
38 "login_url": "https://uk-mynotebook.labarchives.com",
39 "eln_api_url": "https://ukapi.labarchives.com/api",
40 },
41 "eu": {
42 "label": "Europe outside the UK",
43 "login_url": "https://eu-mynotebook.labarchives.com",
44 "eln_api_url": "https://euapi.labarchives.com/api",
45 },
48INVENTORY_API_BASE_URL = "https://iapi.labarchives.com"
50ENV_API_URL = "LABARCHIVES_ELN_API_URL"
51ENV_ACCESS_KEY_ID = "LABARCHIVES_ACCESS_KEY_ID"
52ENV_ACCESS_PASSWORD = "LABARCHIVES_ACCESS_PASSWORD"
53ENV_USER_ID = "LABARCHIVES_USER_ID"
54ENV_INVENTORY_LAB_ID = "LABARCHIVES_INVENTORY_LAB_ID"
56_API_URL_TO_REGION = {
57 values["eln_api_url"]: region for region, values in REGIONS.items()
61class ConfigError(ValueError):
62 """Raised when local LabArchives configuration is invalid."""
65def normalize_eln_api_url(value: str) -> str:
66 """Return an allowlisted ELN API URL or raise ConfigError."""
68 candidate = value.strip()
69 if not candidate:
70 raise ConfigError("ELN API URL is empty")
72 try:
73 parsed = urlsplit(candidate)
74 port = parsed.port
75 except ValueError as exc:
76 raise ConfigError(f"invalid ELN API URL: {exc}") from exc
78 if parsed.scheme.lower() != "https":
79 raise ConfigError("ELN API URL must use https")
80 if parsed.username is not None or parsed.password is not None:
81 raise ConfigError("credentials must not be embedded in the ELN API URL")
82 if port is not None:
83 raise ConfigError("ELN API URL must not specify a custom port")
84 if parsed.query or parsed.fragment:
85 raise ConfigError("ELN API URL must not contain a query or fragment")
86 if parsed.path not in {"/api", "/api/"}:
87 raise ConfigError("ELN API URL path must be exactly /api")
89 host = (parsed.hostname or "").lower()
90 normalized = f"https://{host}/api"
91 if normalized not in _API_URL_TO_REGION:
92 allowed = ", ".join(sorted(_API_URL_TO_REGION))
93 raise ConfigError(f"ELN API URL is not allowlisted; expected one of: {allowed}")
94 return normalized
97def _present(env: Mapping[str, str], name: str) -> bool:
98 return bool(env.get(name, "").strip())
101def _dump(payload: Mapping[str, Any], *, compact: bool, stream: Any) -> None:
102 if compact:
103 json.dump(payload, stream, sort_keys=True, separators=(",", ":"))
104 else:
105 json.dump(payload, stream, indent=2, sort_keys=True)
106 stream.write("\n")
109def _select_api_url(args: argparse.Namespace, env: Mapping[str, str]) -> str:
110 if args.api_url and args.region:
111 raise ConfigError("use either --api-url or --region, not both")
112 if args.api_url:
113 return normalize_eln_api_url(args.api_url)
114 if args.region:
115 return REGIONS[args.region]["eln_api_url"]
117 from_env = env.get(ENV_API_URL, "")
118 if not from_env.strip():
119 raise ConfigError(f"set {ENV_API_URL}, or pass a non-secret --region/--api-url")
120 return normalize_eln_api_url(from_env)
123def command_regions(args: argparse.Namespace) -> int:
124 payload = {
125 "as_of": "2026-09-30",
126 "inventory_documented_api_base_url": INVENTORY_API_BASE_URL,
127 "regions": [
128 {"code": code, **values} for code, values in sorted(REGIONS.items())
129 ],
130 "warning": (
131 "Browser login URLs and ELN API URLs are different. "
132 "Inventory has a separately documented API base; "
133 "regional variants are not inferred."
134 ),
135 }
136 _dump(payload, compact=args.compact, stream=sys.stdout)
137 return 0
140def command_check(
141 args: argparse.Namespace, env: Mapping[str, str] | None = None
142) -> int:
143 selected_env = os.environ if env is None else env
144 api_url = _select_api_url(args, selected_env)
146 access_password_present = _present(selected_env, ENV_ACCESS_PASSWORD)
147 prompted_in_memory = False
148 if not access_password_present and args.prompt_missing_secret:
149 prompted = getpass.getpass(
150 f"{ENV_ACCESS_PASSWORD} (validated in memory; not saved): "
151 )
152 access_password_present = bool(prompted)
153 prompted_in_memory = access_password_present
154 prompted = ""
156 status = {
157 ENV_ACCESS_KEY_ID: _present(selected_env, ENV_ACCESS_KEY_ID),
158 ENV_ACCESS_PASSWORD: access_password_present,
159 ENV_USER_ID: _present(selected_env, ENV_USER_ID),
160 ENV_INVENTORY_LAB_ID: _present(selected_env, ENV_INVENTORY_LAB_ID),
161 }
163 required = [ENV_ACCESS_KEY_ID, ENV_ACCESS_PASSWORD]
164 if args.require_user_id:
165 required.append(ENV_USER_ID)
166 if args.require_inventory_lab_id:
167 required.append(ENV_INVENTORY_LAB_ID)
168 missing = [name for name in required if not status[name]]
170 payload = {
171 "valid": not missing,
172 "region": _API_URL_TO_REGION[api_url],
173 "eln_api_url": api_url,
174 "credential_presence": status,
175 "required_variables": required,
176 "missing_variables": missing,
177 "prompted_secret_kept_in_memory_only": prompted_in_memory,
178 "dotenv_files_read": False,
179 "remote_request_performed": False,
180 }
181 _dump(payload, compact=args.compact, stream=sys.stdout)
182 return 0 if not missing else 2
185def build_parser() -> argparse.ArgumentParser:
186 parser = argparse.ArgumentParser(
187 description=(
188 "Validate official LabArchives ELN regional URLs and the presence "
189 "of named environment variables without printing secrets."
190 )
191 )
192 subparsers = parser.add_subparsers(dest="command", required=True)
194 regions_parser = subparsers.add_parser(
195 "regions", help="print documented browser and ELN API regional URLs"
196 )
197 regions_parser.add_argument(
198 "--compact", action="store_true", help="emit compact JSON"
199 )
200 regions_parser.set_defaults(handler=command_regions)
202 check_parser = subparsers.add_parser(
203 "check", help="validate endpoint selection and credential presence"
204 )
205 endpoint_group = check_parser.add_mutually_exclusive_group()
206 endpoint_group.add_argument(
207 "--region",
208 choices=sorted(REGIONS),
209 help=f"select a documented region instead of reading {ENV_API_URL}",
210 )
211 endpoint_group.add_argument(
212 "--api-url",
213 help="validate a non-secret ELN API URL against the official allowlist",
214 )
215 check_parser.add_argument(
216 "--require-user-id",
217 action="store_true",
218 help=f"require {ENV_USER_ID} to be present",
219 )
220 check_parser.add_argument(
221 "--require-inventory-lab-id",
222 action="store_true",
223 help=f"require {ENV_INVENTORY_LAB_ID} to be present",
224 )
225 check_parser.add_argument(
226 "--prompt-missing-secret",
227 action="store_true",
228 help=(
229 "use getpass for a missing Access Password; validate presence in "
230 "memory only and do not save it"
231 ),
232 )
233 check_parser.add_argument(
234 "--compact", action="store_true", help="emit compact JSON"
235 )
236 check_parser.set_defaults(handler=command_check)
237 return parser
240def main(argv: Sequence[str] | None = None) -> int:
241 parser = build_parser()
242 args = parser.parse_args(argv)
243 try:
244 return int(args.handler(args))
245 except ConfigError as exc:
246 _dump(
247 {
248 "valid": False,
249 "error": str(exc),
250 "remote_request_performed": False,
251 },
252 compact=getattr(args, "compact", False),
253 stream=sys.stderr,
254 )
255 return 2
256 except KeyboardInterrupt:
257 print('{"valid":false,"error":"cancelled"}', file=sys.stderr)
258 return 130
261if __name__ == "__main__":
262 raise SystemExit(main())