Coverage for src/lumen/analytics/client.py: 57%
49 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-08-31 18:46 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-08-31 18:46 +0000
1"""
2Remote HTTP client SDK for Lumen Analytics.
4Allows remote Python clients / Jupyter notebooks running on remote machines
5to query a Lumen FHIR server over HTTP REST and receive pre-flattened
6Pandas DataFrames automatically.
7"""
9from __future__ import annotations
11from typing import Any
13import httpx
14import pandas as pd
17class LumenClient:
18 """Remote HTTP Client for querying Lumen FHIR Server DataFrames."""
20 def __init__(
21 self,
22 base_url: str = "http://localhost:8000",
23 token: str | None = None,
24 timeout: float = 30.0,
25 transport: httpx.BaseTransport | None = None,
26 verify: bool | str = True,
27 ) -> None:
28 self.base_url = base_url.rstrip("/")
29 headers = {"Accept": "application/fhir+json"}
30 if token:
31 headers["Authorization"] = f"Bearer {token}"
32 self._client = httpx.Client(
33 base_url=self.base_url,
34 headers=headers,
35 timeout=timeout,
36 transport=transport,
37 verify=verify,
38 )
40 def search_pandas(
41 self,
42 resource_type: str,
43 params: dict[str, list[str]] | None = None,
44 **kwargs: Any,
45 ) -> pd.DataFrame:
46 """Query the remote Lumen server for a pre-flattened Pandas DataFrame.
48 The server handles database querying and pandas normalization in-process,
49 returning a flat JSON stream that is read directly on the client.
50 """
51 query_params: dict[str, Any] = {}
52 if params:
53 for k, v in params.items():
54 query_params[k] = v
55 for k, v in kwargs.items():
56 norm_k = k.replace("_", "-") if k not in ("_count", "_sort", "_summary") else k
57 query_params[norm_k] = v
59 response = self._client.get(f"/{resource_type}/$search-pandas", params=query_params)
60 response.raise_for_status()
62 # Parse the pre-flattened JSON list of records directly
63 import io
65 return pd.read_json(io.StringIO(response.text), orient="records")
67 def search_arrow(
68 self,
69 resource_type: str,
70 params: dict[str, Any] | None = None,
71 **kwargs: Any,
72 ) -> Any:
73 """Query the remote Lumen server and receive an Apache Arrow Table."""
74 try:
75 import pyarrow as pa
76 except ImportError as exc:
77 raise RuntimeError(
78 "pyarrow is required for search_arrow(). Install it via: pip install pyarrow"
79 ) from exc
81 df = self.search_pandas(resource_type, params=params, **kwargs)
82 return pa.Table.from_pandas(df)
84 def query_duckdb(
85 self,
86 resource_type: str,
87 query: str,
88 params: dict[str, list[str]] | None = None,
89 **kwargs: Any,
90 ) -> pd.DataFrame:
91 """Execute a DuckDB SQL query in-process on the server's DataFrame.
93 The server builds the DataFrame matching the fhir params, registers it
94 as the table name (or 'df'), executes the SQL query using DuckDB,
95 and returns the aggregated result.
96 """
97 fhir_params: dict[str, Any] = {}
98 if params:
99 for k, v in params.items():
100 fhir_params[k] = v
101 for k, v in kwargs.items():
102 norm_k = k.replace("_", "-") if k not in ("_count", "_sort", "_summary") else k
103 fhir_params[norm_k] = v
105 payload = {"query": query, "params": fhir_params}
107 response = self._client.post(f"/{resource_type}/$search-duckdb", json=payload)
108 response.raise_for_status()
110 import io
112 return pd.read_json(io.StringIO(response.text), orient="records")
114 def close(self) -> None:
115 self._client.close()
117 def __enter__(self) -> LumenClient:
118 return self
120 def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
121 self.close()