From b0d3ba3eedf8cc782439423e7f7a8de5843e869e Mon Sep 17 00:00:00 2001 From: Arun Date: Mon, 21 Sep 2026 15:16:09 +0530 Subject: [PATCH] support forwarding vitals and syncing patients to multiple configurable URLs --- api_client.py | 112 +++++++++++++++++++++++++------------------------- config.json | 4 ++ config.py | 4 ++ 3 files changed, 64 insertions(+), 56 deletions(-) diff --git a/api_client.py b/api_client.py index 2a3bfb3..0e817d5 100644 --- a/api_client.py +++ b/api_client.py @@ -41,69 +41,54 @@ async def forward_vitals_to_api(vitals: NormalizedVitals) -> bool: client = get_http_client() - # 1. Forward to primary target API (e.g. Injomo Dev Workflow) - async def _forward_primary() -> bool: - primary_url = settings.target_api_url - if not primary_url: + async def _post_target(url: str, label: str) -> bool: + if not url: + return False + curr_port = os.getenv("PORT", "8000") + if f":{curr_port}" in url and ("localhost" in url or "127.0.0.1" in url): return False try: - primary_headers = headers.copy() + req_headers = headers.copy() if settings.api_token: - primary_headers["Authorization"] = f"Bearer {settings.api_token}" - response = await client.post( - primary_url, - json=payload, - headers=primary_headers - ) + req_headers["Authorization"] = f"Bearer {settings.api_token}" + url_to_call = url + if settings.api_token and "?" not in url and "/api/push-vitals" in url: + url_to_call = f"{url}?api_key={settings.api_token}" + response = await client.post(url_to_call, json=payload, headers=req_headers) response.raise_for_status() - logger.info(f"Successfully forwarded vitals to primary API for device {vitals.device_id}") + logger.info(f"Successfully forwarded vitals to {label} ({url}) for device {vitals.device_id}") return True except Exception as e: err_msg = str(e).strip() or "timeout / connection reset" - logger.warning(f"Error forwarding to primary API: {type(e).__name__} ({err_msg})") + logger.warning(f"Error forwarding to {label} ({url}): {type(e).__name__} ({err_msg})") return False - # 2. Forward to Cloud Server - async def _forward_cloud() -> bool: - cloud_base = get_cloud_base_url() - cloud_url = f"{cloud_base}/api/push-vitals" - is_on_cloud = os.getenv("RENDER") is not None or os.getenv("PORT") == "10000" or "render.local" in os.getenv("HOSTNAME", "") - if is_on_cloud and ("onrender.com" in cloud_base): - return True - try: - url_with_key = cloud_url - if settings.api_token: - url_with_key = f"{cloud_url}?api_key={settings.api_token}" - - response = await client.post( - url_with_key, - json=payload, - headers=headers - ) - response.raise_for_status() - logger.info(f"Successfully forwarded vitals to Cloud API for device {vitals.device_id}") - return True - except Exception as e: - err_msg = str(e).strip() or "timeout / connection reset" - logger.warning(f"Error forwarding to Cloud API: {type(e).__name__} ({err_msg})") - return False + targets = [] + if settings.target_api_url: + targets.append((settings.target_api_url, "Primary Target API")) - results = await asyncio.gather(_forward_primary(), _forward_cloud(), return_exceptions=True) + cloud_base = get_cloud_base_url() + is_on_cloud = os.getenv("RENDER") is not None or os.getenv("PORT") == "10000" or "render.local" in os.getenv("HOSTNAME", "") + if not (is_on_cloud and "onrender.com" in cloud_base): + cloud_push_url = f"{cloud_base}/api/push-vitals" if not cloud_base.endswith("/api/push-vitals") else cloud_base + if cloud_push_url not in [t[0] for t in targets]: + targets.append((cloud_push_url, "Cloud Server")) + + for extra_url in getattr(settings, "forward_urls", []): + if extra_url and extra_url not in [t[0] for t in targets]: + targets.append((extra_url, "Configured Forward URL")) + + tasks = [_post_target(url, lbl) for url, lbl in targets] + results = await asyncio.gather(*tasks, return_exceptions=True) return any(r is True for r in results if not isinstance(r, Exception)) async def sync_patient_assignment_to_cloud(patient_data: dict, device_id: str) -> bool: """ - Synchronizes patient demographics assignment to Cloud Server. + Synchronizes patient demographics assignment to Cloud and LAN servers. Ensures local and remote dashboards always show the exact same patient. """ - cloud_base = get_cloud_base_url() - is_on_cloud = os.getenv("RENDER") is not None or os.getenv("PORT") == "10000" or "render.local" in os.getenv("HOSTNAME", "") - if is_on_cloud and ("onrender.com" in cloud_base): - return True - client = get_http_client() - cloud_assign_url = f"{cloud_base}/api/patient/assign" payload = { "device_id": device_id, "mrn": patient_data.get("mrn") or patient_data.get("patient_id") or "", @@ -113,14 +98,29 @@ async def sync_patient_assignment_to_cloud(patient_data: dict, device_id: str) - "bed_id": patient_data.get("bed_id"), "patient_type": patient_data.get("patient_type") } - try: - url_with_key = cloud_assign_url - if settings.api_token: - url_with_key = f"{cloud_assign_url}?api_key={settings.api_token}" - resp = await client.post(url_with_key, json=payload, headers={"Content-Type": "application/json"}) - resp.raise_for_status() - logger.info(f"Successfully synced patient {payload['mrn']} ({payload['name']}) to Cloud for {device_id}") - return True - except Exception as e: - logger.warning(f"Could not sync patient assignment to Cloud: {type(e).__name__} ({e})") - return False + + targets = [] + cloud_base = get_cloud_base_url() + targets.append(f"{cloud_base}/api/patient/assign") + + for extra_url in getattr(settings, "forward_urls", []): + if "/api/push-vitals" in extra_url: + assign_url = extra_url.replace("/api/push-vitals", "/api/patient/assign") + if assign_url not in targets: + targets.append(assign_url) + + async def _sync_one(url: str) -> bool: + try: + url_with_key = url + if settings.api_token: + url_with_key = f"{url}?api_key={settings.api_token}" + resp = await client.post(url_with_key, json=payload, headers={"Content-Type": "application/json"}) + resp.raise_for_status() + logger.info(f"Successfully synced patient {payload['mrn']} to {url} for {device_id}") + return True + except Exception as e: + logger.warning(f"Could not sync patient assignment to {url}: {type(e).__name__} ({e})") + return False + + results = await asyncio.gather(*[_sync_one(u) for u in targets], return_exceptions=True) + return any(r is True for r in results if not isinstance(r, Exception)) diff --git a/config.json b/config.json index 6ec75ef..9ae0d48 100644 --- a/config.json +++ b/config.json @@ -9,6 +9,10 @@ }, "target_api_url": "https://services-api.a2zhealth.in/v1/contec-monitor", "cloud_api_url": "https://services-api.a2zhealth.in/v1/contec-monitor", + "forward_urls": [ + "http://192.168.100.104:10000/api/push-vitals", + "https://services-api.a2zhealth.in/v1/contec-monitor" + ], "api_token": "", "retry_interval_seconds": 60, "log_file": "hl7_forwarder.log", diff --git a/config.py b/config.py index 8aadcc9..5b9410e 100644 --- a/config.py +++ b/config.py @@ -11,6 +11,10 @@ class Settings(BaseSettings): device_models: dict = {} target_api_url: str = "https://services-api.a2zhealth.in/v1/contec-monitor" cloud_api_url: str = "https://services-api.a2zhealth.in/v1/contec-monitor" + forward_urls: list = [ + "http://192.168.100.104:10000/api/push-vitals", + "https://services-api.a2zhealth.in/v1/contec-monitor" + ] api_token: str = "" retry_interval_seconds: int = 60 log_file: str = "hl7_forwarder.log"