Skip to content

Commit c0049b0

Browse files
jenniferjiangkellsJennifer Jiang-Kellsadamkells
authored
Big refactor energy (#46)
* ♻️ Big refactoring * Update documentation * Fixed weird merge duplication in quickstart * readding free text parser * removing redundant parser code --------- Co-authored-by: Jennifer Jiang-Kells <[email protected]> Co-authored-by: Adam Kells <[email protected]>
1 parent 8aaaaf0 commit c0049b0

File tree

81 files changed

+1141
-1026
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

81 files changed

+1141
-1026
lines changed

docs/quickstart.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ You can use the data generator within a client function or on its own. The `.gen
9090
import healthchain as hc
9191
from healthchain.use_cases import ClinicalDecisionSupport
9292
from healthchain.models import CdsFhirData
93-
from healthchain.data_generator import CdsDataGenerator
93+
from healthchain.data_generators import CdsDataGenerator
9494

9595
@hc.sandbox
9696
class MyCoolSandbox(ClinicalDecisionSupport):
@@ -110,8 +110,8 @@ You can use the data generator within a client function or on its own. The `.gen
110110

111111
=== "On its own"
112112
```python
113-
from healthchain.data_generator import CdsDataGenerator
114-
from healthchain.base import Workflow
113+
from healthchain.data_generators import CdsDataGenerator
114+
from healthchain.workflow import Workflow
115115

116116
# Initialise data generator
117117
data_generator = CdsDataGenerator()
@@ -156,7 +156,7 @@ A random text document from the `csv` file will be picked for each generation.
156156

157157
```python
158158
# Load free text into a DocumentResource FHIR resource
159-
data_generator.generate(free_text_csv="./dir/to/csv/file")
159+
data = data_generator.generate(free_text_csv="./dir/to/csv/file")
160160
```
161161

162162

@@ -177,7 +177,7 @@ If you are using a model that requires initialisation steps, we recommend you in
177177
import healthchain as hc
178178

179179
from healthchain.use_cases import ClinicalDecisionSupport
180-
from healthchain.data_generator import CdsDataGenerator
180+
from healthchain.data_generators import CdsDataGenerator
181181
from healthchain.models import Card, CDSRequest, CdsFhirData
182182
from transformers import pipeline
183183

@@ -218,8 +218,8 @@ If you are using a model that requires initialisation steps, we recommend you in
218218
import healthchain as hc
219219

220220
from healthchain.use_cases import ClinicalDecisionSupport
221-
from healthchain.data_generator import CdsDataGenerator
222-
from healthchain.models import Card, CdsFhirData, CDSRequest
221+
from healthchain.data_generators import CdsDataGenerator
222+
from healthchain.models import Card, CDSRequest, CdsFhirData
223223

224224
from langchain_openai import ChatOpenAI
225225
from langchain_core.prompts import PromptTemplate

healthchain/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import logging
22
from .utils.logger import add_handlers
3-
from healthchain.decorators import ehr, api, sandbox
4-
from healthchain.data_generator.data_generator import CdsDataGenerator
5-
from healthchain.models.requests.cdsrequest import CDSRequest
3+
4+
from .decorators import api, sandbox
5+
from .clients import ehr
66

77
logger = logging.getLogger(__name__)
88
add_handlers(logger)
99
logger.setLevel(logging.INFO)
1010

1111
# Export them at the top level
12-
__all__ = ["ehr", "api", "sandbox", "CdsDataGenerator", "CDSRequest"]
12+
__all__ = ["ehr", "api", "sandbox"]

healthchain/base.py

Lines changed: 2 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,8 @@
11
from abc import ABC, abstractmethod
2-
from enum import Enum
32
from typing import Dict
43

5-
from .utils.endpoints import Endpoint
6-
7-
8-
# a workflow is a specific event that may occur in an EHR that triggers a request to server
9-
class Workflow(Enum):
10-
patient_view = "patient-view"
11-
order_select = "order-select"
12-
order_sign = "order-sign"
13-
encounter_discharge = "encounter-discharge"
14-
notereader_sign_inpatient = "notereader-sign-inpatient"
15-
notereader_sign_outpatient = "notereader-sign-outpatient"
16-
17-
18-
class UseCaseType(Enum):
19-
cds = "ClinicalDecisionSupport"
20-
clindoc = "ClinicalDocumentation"
21-
22-
23-
class UseCaseMapping(Enum):
24-
ClinicalDecisionSupport = (
25-
"patient-view",
26-
"order-select",
27-
"order-sign",
28-
"encounter-discharge",
29-
)
30-
ClinicalDocumentation = ("notereader-sign-inpatient", "notereader-sign-outpatient")
31-
32-
def __init__(self, *workflows):
33-
self.allowed_workflows = workflows
34-
35-
36-
def is_valid_workflow(use_case: UseCaseMapping, workflow: Workflow) -> bool:
37-
return workflow.value in use_case.allowed_workflows
38-
39-
40-
def validate_workflow(use_case: UseCaseMapping):
41-
def decorator(func):
42-
def wrapper(*args, **kwargs):
43-
if len(kwargs) > 0:
44-
workflow = kwargs.get("workflow")
45-
else:
46-
for arg in args:
47-
if type(arg) == Workflow:
48-
workflow = arg
49-
if not is_valid_workflow(use_case, workflow):
50-
raise ValueError(f"Invalid workflow {workflow} for UseCase {use_case}")
51-
return func(*args, **kwargs)
52-
53-
return wrapper
54-
55-
return decorator
4+
from .workflows import UseCaseType, Workflow
5+
from .service.endpoints import Endpoint
566

577

588
class BaseClient(ABC):

healthchain/clients.py

Lines changed: 0 additions & 84 deletions
This file was deleted.

healthchain/clients/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .ehrclient import ehr
2+
3+
__all__ = ["ehr"]

healthchain/clients/ehrclient.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import logging
2+
import httpx
3+
4+
from typing import Any, Callable, List, Dict, Optional, Union, TypeVar
5+
from functools import wraps
6+
7+
from healthchain.data_generators import CdsDataGenerator
8+
from healthchain.decorators import assign_to_attribute, find_attributes_of_type
9+
from healthchain.workflows import UseCaseType, Workflow
10+
from healthchain.models import CDSRequest
11+
from healthchain.base import BaseStrategy, BaseClient, BaseUseCase
12+
13+
log = logging.getLogger(__name__)
14+
15+
F = TypeVar("F", bound=Callable)
16+
17+
18+
def ehr(
19+
func: Optional[F] = None, *, workflow: Workflow, num: int = 1
20+
) -> Union[Callable[..., Any], Callable[[F], F]]:
21+
"""
22+
A decorator that wraps around a data generator function and returns an EHRClient
23+
24+
Parameters:
25+
func (Optional[Callable]): The function to be decorated. If None, this allows the decorator to
26+
be used with arguments.
27+
workflow ([str]): The workflow identifier which should match an item in the Workflow enum.
28+
This specifies the context in which the EHR function will operate.
29+
num (int): The number of requests to generate in the queue; defaults to 1.
30+
31+
Returns:
32+
Callable: A decorated callable that incorporates EHR functionality or the decorator itself
33+
if 'func' is None, allowing it to be used as a parameterized decorator.
34+
35+
Raises:
36+
ValueError: If the workflow does not correspond to any defined enum or if use case is not configured.
37+
NotImplementedError: If the use case class is not one of the supported types.
38+
39+
Example:
40+
@ehr(workflow='patient-view', num=2)
41+
def generate_data(self, config):
42+
# Function implementation
43+
"""
44+
45+
def decorator(func: F) -> F:
46+
func.is_client = True
47+
48+
@wraps(func)
49+
def wrapper(self, *args: Any, **kwargs: Any) -> EHRClient:
50+
assert issubclass(
51+
type(self), BaseUseCase
52+
), f"{self.__class__.__name__} must be subclass of valid Use Case strategy!"
53+
54+
try:
55+
workflow_enum = Workflow(workflow)
56+
except ValueError as e:
57+
raise ValueError(
58+
f"{e}: please select from {[x.value for x in Workflow]}"
59+
)
60+
61+
# Set workflow in data generator if configured
62+
data_generator_attributes = find_attributes_of_type(self, CdsDataGenerator)
63+
for i in range(len(data_generator_attributes)):
64+
attribute_name = data_generator_attributes[i]
65+
try:
66+
assign_to_attribute(
67+
self, attribute_name, "set_workflow", workflow_enum
68+
)
69+
except Exception as e:
70+
log.error(
71+
f"Could not set workflow {workflow_enum.value} for data generator method {attribute_name}: {e}"
72+
)
73+
if i > 1:
74+
log.warning("More than one DataGenerator instances found.")
75+
76+
if self.type in UseCaseType:
77+
method = EHRClient(func, workflow=workflow_enum, strategy=self.strategy)
78+
for _ in range(num):
79+
method.generate_request(self, *args, **kwargs)
80+
else:
81+
raise NotImplementedError(
82+
f"Use case {self.type} not recognised, check if implemented."
83+
)
84+
return method
85+
86+
return wrapper
87+
88+
if func is None:
89+
return decorator
90+
else:
91+
return decorator(func)
92+
93+
94+
class EHRClient(BaseClient):
95+
def __init__(
96+
self, func: Callable[..., Any], workflow: Workflow, strategy: BaseStrategy
97+
):
98+
"""
99+
Initializes the EHRClient with a data generator function and optional workflow and use case.
100+
101+
Parameters:
102+
func (Callable[..., Any]): A function to generate data for requests.
103+
workflow ([Workflow]): The workflow context to apply to the data generator.
104+
use_case ([BaseUseCase]): The strategy object to construct requests based on the generated data.
105+
Should be a subclass of BaseUseCase. Example - ClinicalDecisionSupport()
106+
"""
107+
self.data_generator_func: Callable[..., Any] = func
108+
self.workflow: Workflow = workflow
109+
self.strategy: BaseStrategy = strategy
110+
self.request_data: List[CDSRequest] = []
111+
112+
def generate_request(self, *args: Any, **kwargs: Any) -> None:
113+
"""
114+
Generates a request using the data produced by the data generator function,
115+
and appends it to the internal request queue.
116+
117+
Parameters:
118+
*args (Any): Positional arguments passed to the data generator function.
119+
**kwargs (Any): Keyword arguments passed to the data generator function.
120+
121+
Raises:
122+
ValueError: If the use case is not configured.
123+
"""
124+
data = self.data_generator_func(*args, **kwargs)
125+
self.request_data.append(self.strategy.construct_request(data, self.workflow))
126+
127+
async def send_request(self, url: str) -> List[Dict]:
128+
"""
129+
Sends all queued requests to the specified URL and collects the responses.
130+
131+
Parameters:
132+
url (str): The URL to which the requests will be sent.
133+
Returns:
134+
List[dict]: A list of JSON responses from the server.
135+
Notes:
136+
This method logs errors rather than raising them, to avoid interrupting the batch processing of requests.
137+
"""
138+
139+
async with httpx.AsyncClient() as client:
140+
json_responses: List[Dict] = []
141+
for request in self.request_data:
142+
try:
143+
# TODO: pass timeout as config
144+
timeout = httpx.Timeout(10.0, read=None)
145+
response = await client.post(
146+
url=url,
147+
json=request.model_dump(exclude_none=True),
148+
timeout=timeout,
149+
)
150+
response.raise_for_status()
151+
json_responses.append(response.json())
152+
except httpx.HTTPStatusError as exc:
153+
log.error(
154+
f"Error response {exc.response.status_code} while requesting {exc.request.url!r}."
155+
)
156+
json_responses.append({})
157+
except httpx.TimeoutException as exc:
158+
log.error(f"Request to {exc.request.url!r} timed out!")
159+
json_responses.append({})
160+
except httpx.RequestError as exc:
161+
log.error(
162+
f"An error occurred while requesting {exc.request.url!r}."
163+
)
164+
json_responses.append({})
165+
166+
return json_responses

0 commit comments

Comments
 (0)