from __future__ import annotations from dataclasses import dataclass import os from typing import Any, Dict def _env(name: str, default: str) -> str: v = os.getenv(name, "").strip() return v or default def _env_float(name: str, default: float) -> float: v = os.getenv(name, "").strip() if not v: return float(default) try: return float(v) except Exception: return float(default) @dataclass(frozen=True) class ServiceConfig: hardware: str interp: str analysis: str SERVICES = ServiceConfig( hardware=_env("HARDWARE_BASE_URL", "http://127.0.0.1:8000"), interp=_env("INTERP_BASE_URL", "http://127.0.0.1:7475"), analysis=_env("ANALYSIS_BASE_URL", "http://127.0.0.1:1717"), ) HARDWARE_BASE_URL: str = SERVICES.hardware INTERP_BASE_URL: str = SERVICES.interp ANALYSIS_BASE_URL: str = SERVICES.analysis BASE_URL: str = HARDWARE_BASE_URL HTTP_TIMEOUT_SEC: float = _env_float("HTTP_TIMEOUT_SEC", 5.0) ENDPOINTS: Dict[str, Any] = { # ----------------------------- # scanner / systems / sequences # ----------------------------- "sequence_params": "/sequence/params", # POST sequence params JSON "sequence_interpret": { "interpret": "/interpret/", # POST multipart file (.seq) "status": "/status/", # GET status of interpretation tasks }, "systems": { "gradient": { "health": "/systems/gradient/health", # GET "upload_config": "/systems/gradient/config", # POST multipart file "start": "/systems/gradient/start", # POST }, "rf": { "health": "/systems/rf/health", "upload_config": "/systems/rf/config", "start": "/systems/rf/start", }, "adc": { "health": "/systems/adc/health", "upload_config": "/systems/adc/config", "start": "/systems/adc/start", }, }, "scan": { # Real spectrometer service API (measurement-based) "start": "/api/measurement", # POST (or GET fallback) -> measurement id "start_fallback_get": "/api/measurement", # GET starts measurement on some deployments "state_by_id": "/api/measurement/{scan_id}/state", # GET "data_by_id": "/api/measurement/{scan_id}/data", # GET "state": "/api/mstate", # GET optional legacy state endpoint # Backward compatibility for older backend shape "progress": "/scan/progress", # GET params: scan_id=... "stop": "/scan/stop", # POST optional }, # ----------------------------- # ANALYSIS # ----------------------------- "analysis": { "upload": "/upload/", # POST multipart "filter": "/filter/", # POST form "fft": "/fft/", # POST form "result": "/result/", # GET params session_id "export": "/export/", # POST form "plot_raw": "/plot-raw/", # POST form # readme_spectrum endpoints "export_raw_data": "/export-raw-data/", # POST form "export_filter_data": "/export-filter-data/", # POST form "export_decdem_data": "/export-decdem-data/", # POST form "export_position_freq": "/export-position-freq/", # POST form "export_fwhm": "/export-FWHM/", # POST form "export_max_amplitude_freq": "/export-max-amplitude-freq/",# POST form }, } def join_url(base_url: str, path: str) -> str: b = (base_url or "").rstrip("/") p = (path or "") if not p.startswith("/"): p = "/" + p return b + p def build_url(service: str, path: str) -> str: base = getattr(SERVICES, service) return join_url(base, path) def get_endpoint(*keys: str) -> str: """Resolve endpoint path from ENDPOINTS by keys. Examples: get_endpoint("sequence_params") get_endpoint("systems", "gradient", "health") get_endpoint("analysis", "upload") """ cur: Any = ENDPOINTS for k in keys: cur = cur[k] if not isinstance(cur, str): raise TypeError(f"Endpoint {keys} resolved to non-string: {type(cur)}") return cur