from __future__ import annotations import os from dataclasses import dataclass from typing import Any, Dict, Optional import requests from services.analysis_service import AnalysisService as AnalysisApi @dataclass(frozen=True) class ApiResult: ok: bool status_code: int data: Any = None error: Optional[str] = None class ApiClient: def __init__(self, base_url: str, timeout_sec: float = 5.0) -> None: self.base_url = base_url.rstrip("/") self.timeout_sec = float(timeout_sec) self.session = requests.Session() def _url(self, path: str) -> str: if not path.startswith("/"): path = "/" + path return f"{self.base_url}{path}" def _request(self, method: str, path: str, **kwargs: Any) -> ApiResult: url = self._url(path) try: r = self.session.request(method=method, url=url, timeout=self.timeout_sec, **kwargs) sc = r.status_code r.raise_for_status() return ApiResult(ok=True, status_code=sc, data=(r.json() if r.content else None)) except Exception as e: sc = getattr(getattr(e, "response", None), "status_code", 0) return ApiResult(ok=False, status_code=sc or 0, error=str(e)) def get_json(self, path: str, params: Optional[Dict[str, Any]] = None) -> ApiResult: return self._request("GET", path, params=params) def post_json(self, path: str, payload: Dict[str, Any]) -> ApiResult: return self._request("POST", path, json=payload) def post_file(self, path: str, file_path: str, field_name: str = "file") -> ApiResult: try: with open(file_path, "rb") as f: files = {field_name: (os.path.basename(file_path), f)} return self._request("POST", path, files=files) except Exception as e: sc = getattr(getattr(e, "response", None), "status_code", 0) return ApiResult(ok=False, status_code=sc or 0, error=str(e)) class LoggedApiClient(ApiClient): def _full_url(self, endpoint: str) -> str: endpoint = endpoint if endpoint.startswith("/") else f"/{endpoint}" return f"{self.base_url}{endpoint}" def get_json(self, endpoint: str, params: Optional[dict] = None) -> ApiResult: print(f"[HTTP][GET] {self._full_url(endpoint)} params={params}") return super().get_json(endpoint, params=params) def post_json(self, endpoint: str, payload: dict) -> ApiResult: print(f"[HTTP][POST] {self._full_url(endpoint)} payload={payload}") return super().post_json(endpoint, payload) def post_file(self, endpoint: str, file_path: str, field_name: str = "file") -> ApiResult: if os.path.exists(file_path): size = os.path.getsize(file_path) else: size = -1 print(f"[HTTP][UPLOAD] {self._full_url(endpoint)} file={file_path} size={size}") return super().post_file(endpoint, file_path, field_name) class LoggedAnalysisApi: def __init__(self, base_url: str, timeout: float): self.base_url = base_url.rstrip("/") self.timeout = timeout def _api(self) -> AnalysisApi: return AnalysisApi(self.base_url, timeout=self.timeout) def upload(self, file_path: str) -> str: print(f"[ANALYSIS][POST] {self.base_url}/upload file={file_path}") return self._api().upload(file_path) def filter(self, session_id: str, dt: float, center: float, lo: float, hi: float, low: float) -> dict: print(f"[ANALYSIS][POST] {self.base_url}/filter sid={session_id}") return self._api().filter(session_id, dt, center, lo, hi, low) def fft( self, session_id: str, c1: Optional[int], c2: Optional[int], c3: Optional[int], c4: Optional[int], ) -> dict: print(f"[ANALYSIS][POST] {self.base_url}/fft sid={session_id} c=({c1},{c2},{c3},{c4})") return self._api().fft(session_id, c1=c1, c2=c2, c3=c3, c4=c4) def result(self, session_id: str) -> dict: print(f"[ANALYSIS][GET] {self.base_url}/result/{session_id}") return self._api().result(session_id) def export_plots(self, session_id: str) -> dict: print(f"[ANALYSIS][GET] {self.base_url}/export/{session_id}") return self._api().export_plots(session_id) def plot_raw(self, session_id: str) -> dict: print(f"[ANALYSIS][GET] {self.base_url}/plot-raw/{session_id}") return self._api().plot_raw(session_id) def export_raw_data(self, session_id: str) -> dict: print(f"[ANALYSIS][GET] {self.base_url}/export-raw-data/{session_id}") return self._api().export_raw_data(session_id) def export_filter_data( self, session_id: str, center_freq: float, lower_freq: float, higher_freq: float, low_freq: float, ) -> dict: print(f"[ANALYSIS][GET] {self.base_url}/export-filter-data/{session_id}") return self._api().export_filter_data( session_id, center_freq=center_freq, lower_freq=lower_freq, higher_freq=higher_freq, low_freq=low_freq, ) def export_decdem_data(self, session_id: str) -> dict: print(f"[ANALYSIS][GET] {self.base_url}/export-decdem-data/{session_id}") return self._api().export_decdem_data(session_id) def export_position_freq(self, session_id: str) -> dict: print(f"[ANALYSIS][GET] {self.base_url}/export-position-freq/{session_id}") return self._api().export_position_freq(session_id) def export_fwhm(self, session_id: str) -> dict: print(f"[ANALYSIS][GET] {self.base_url}/export-fwhm/{session_id}") return self._api().export_fwhm(session_id) def export_max_amplitude_freq(self, session_id: str) -> dict: print(f"[ANALYSIS][GET] {self.base_url}/export-max-amplitude-freq/{session_id}") return self._api().export_max_amplitude_freq(session_id) def download_bytes(self, rel_url: str) -> bytes: url = f"{self.base_url.rstrip('/')}/{rel_url.lstrip('/')}" print(f"[ANALYSIS][GET-BYTES] {url}") r = requests.get(url, timeout=self.timeout) r.raise_for_status() return r.content @dataclass class AnalysisState: base_url: str session_id: str = "" last_uploaded_path: str = ""