127 lines
5.3 KiB
Python
127 lines
5.3 KiB
Python
import httpx
|
|
import logging
|
|
import asyncio
|
|
import os
|
|
from typing import Optional
|
|
from schemas import NormalizedVitals
|
|
from config import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_http_client: Optional[httpx.AsyncClient] = None
|
|
|
|
def get_http_client() -> httpx.AsyncClient:
|
|
global _http_client
|
|
if _http_client is None or _http_client.is_closed:
|
|
_http_client = httpx.AsyncClient(
|
|
timeout=httpx.Timeout(connect=5.0, read=5.0, write=5.0, pool=10.0),
|
|
limits=httpx.Limits(max_keepalive_connections=20, max_connections=50)
|
|
)
|
|
return _http_client
|
|
|
|
def get_cloud_base_url() -> str:
|
|
url = getattr(settings, "cloud_api_url", None) or os.getenv("CLOUD_API_URL") or "https://services-api.a2zhealth.in/v1/contec-monitor"
|
|
return url.rstrip("/")
|
|
|
|
async def forward_vitals_to_api(vitals: NormalizedVitals) -> bool:
|
|
"""
|
|
Attempts to POST the normalized vitals to the configured target API
|
|
and to the Cloud server concurrently.
|
|
Returns True if at least one forward succeeds.
|
|
"""
|
|
headers = {"Content-Type": "application/json"}
|
|
|
|
# Strip heavy wave sample arrays before REST transmission (cloud APIs only ingest vital signs)
|
|
payload = vitals.model_dump(mode="json")
|
|
payload.pop("ecg_wave", None)
|
|
payload.pop("pleth_wave", None)
|
|
|
|
if not payload.get("patient_id"):
|
|
payload["patient_id"] = "UNKNOWN"
|
|
|
|
client = get_http_client()
|
|
|
|
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:
|
|
req_headers = headers.copy()
|
|
if settings.api_token:
|
|
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 {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 {label} ({url}): {type(e).__name__} ({err_msg})")
|
|
return False
|
|
|
|
targets = []
|
|
if settings.target_api_url:
|
|
targets.append((settings.target_api_url, "Primary Target API"))
|
|
|
|
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 and LAN servers.
|
|
Ensures local and remote dashboards always show the exact same patient.
|
|
"""
|
|
client = get_http_client()
|
|
payload = {
|
|
"device_id": device_id,
|
|
"mrn": patient_data.get("mrn") or patient_data.get("patient_id") or "",
|
|
"name": patient_data.get("name") or patient_data.get("patient_name") or "Unknown",
|
|
"dob": patient_data.get("dob") or patient_data.get("patient_dob"),
|
|
"gender": patient_data.get("gender") or patient_data.get("patient_gender"),
|
|
"bed_id": patient_data.get("bed_id"),
|
|
"patient_type": patient_data.get("patient_type")
|
|
}
|
|
|
|
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))
|