diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 34e0762..9029476 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,9 @@ jobs: with: python-version: ${{matrix.python-version}} poetry-version: ${{matrix.poetry-version}} + - name: Install package + run: | + poetry install --no-interaction - name: Lint with Ruff run: | poetry run ruff format diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a0a5a7e --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,19 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.4.2 + hooks: + # Run the linter. + - id: ruff + args: [ --fix ] + # Run the formatter. + - id: ruff-format diff --git a/README.md b/README.md index b1ee71d..6a888b8 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ ![GitHub License](https://img.shields.io/github/license/:user/:repo) -# DoppelData +# HealthChain -A package to help Healthtech developers generate synthetic data in the Fhir data standard. +Simplify prototyping and testing LLM applications in healthcare context. ## Quickstart diff --git a/docs/index.md b/docs/index.md index f8fc1fb..9802c01 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1 +1 @@ -# DoppelData \ No newline at end of file +# DoppelData diff --git a/example_use.py b/example_use.py new file mode 100644 index 0000000..648bf82 --- /dev/null +++ b/example_use.py @@ -0,0 +1,56 @@ +from healthchain.use_cases.cds import ClinicalDecisionSupport +from healthchain.decorators import ehr +import dataclasses +import uuid + + +def Run(): + # ehr = EHR() + # ehr = EHR.from_doppeldata(data) + # ehr = EHR.from_path(path) + + # ehr.UseCase = ClinicalDecisionSupport() + # print(ehr.UseCase) + + # ehr.add_database(data) + + # ehr.send_request("http://0.0.0.0:8000", Workflow("patient-view")) + # ehr.send_request("http://0.0.0.0:8000", Workflow("notereader-sign-inpatient")) + + @dataclasses.dataclass + class synth_data: + context: dict + uuid: str + prefetch: dict + + # @sandbox(use_case=ClinicalDecisionSupport()) + class myCDS: + def __init__(self) -> None: + self.data_generator = None + self.use_case = ClinicalDecisionSupport() + + # decorator sets up an instance of ehr configured with use case CDS + @ehr(workflow="patient-view", num=5) + def load_data(self, data_spec): + # data = "hello, " + data_spec + data = synth_data( + context={"userId": "Practitioner/123", "patientId": data_spec}, + uuid=str(uuid.uuid4()), + prefetch={}, + ) + return data + + # @service(langserve=True) + # def llm(self): + # chain = llm | output_parser + # return chain + + cds = myCDS() + ehr_client = cds.load_data("123") + request = ehr_client.request_data + for i in range(len(request)): + print(request[i].model_dump_json(exclude_none=True)) + + +if __name__ == "__main__": + Run() diff --git a/healthchain/__init__.py b/healthchain/__init__.py new file mode 100644 index 0000000..5826d2b --- /dev/null +++ b/healthchain/__init__.py @@ -0,0 +1,6 @@ +import logging +from .utils.logger import ColorLogger + + +logging.setLoggerClass(ColorLogger) +logger = logging.getLogger(__name__) diff --git a/healthchain/base.py b/healthchain/base.py new file mode 100644 index 0000000..e8c1594 --- /dev/null +++ b/healthchain/base.py @@ -0,0 +1,83 @@ +from abc import ABC, abstractmethod +from enum import Enum +from typing import Dict + + +# a workflow is a specific event that may occur in an EHR that triggers a request to server +class Workflow(Enum): + patient_view = "patient-view" + order_select = "order-select" + order_sign = "order-sign" + encounter_discharge = "encounter-discharge" + notereader_sign_inpatient = "notereader-sign-inpatient" + notereader_sign_outpatient = "notereader-sign-outpatient" + + +class UseCaseType(Enum): + cds = "ClinicalDecisionSupport" + clindoc = "ClinicalDocumentation" + + +class UseCaseMapping(Enum): + ClinicalDecisionSupport = ( + "patient-view", + "order-select", + "order-sign", + "encounter-discharge", + ) + ClinicalDocumentation = ("notereader-sign-inpatient", "notereader-sign-outpatient") + + def __init__(self, *workflows): + self.allowed_workflows = workflows + + +def is_valid_workflow(use_case: UseCaseMapping, workflow: Workflow) -> bool: + return workflow.value in use_case.allowed_workflows + + +def validate_workflow(use_case: UseCaseMapping): + def decorator(func): + def wrapper(*args, **kwargs): + if len(kwargs) > 0: + workflow = kwargs.get("workflow") + else: + for arg in args: + if type(arg) == Workflow: + workflow = arg + if not is_valid_workflow(use_case, workflow): + raise ValueError(f"Invalid workflow {workflow} for UseCase {use_case}") + return func(*args, **kwargs) + + return wrapper + + return decorator + + +class BaseClient(ABC): + """Base client class + A client can be an EHR or CPOE etc. + The basic operation is that it sends data in a specified standard. + """ + + @abstractmethod + def send_request(self) -> None: + """ + Sends a request to AI service + """ + + +class BaseUseCase(ABC): + """ + Abstract class for a specific use case of an EHR object + Use cases will differ by: + - the data it accepts (FHIR or CDA) + - the format of the request it constructs (CDS Hook or NoteReader workflows) + """ + + @abstractmethod + def _validate_data(self, data) -> bool: + pass + + @abstractmethod + def construct_request(self, data, workflow: Workflow) -> Dict: + pass diff --git a/healthchain/clients.py b/healthchain/clients.py new file mode 100644 index 0000000..a9daaa1 --- /dev/null +++ b/healthchain/clients.py @@ -0,0 +1,66 @@ +import logging +import requests + +from typing import Any, Callable, List, Dict + +from .base import BaseUseCase, BaseClient, Workflow + +log = logging.getLogger(__name__) + + +class EHRClient(BaseClient): + def __init__( + self, func: Callable[..., Any], workflow: Workflow, use_case: BaseUseCase + ): + """ + Initializes the EHRClient with a data generator function and optional workflow and use case. + + Parameters: + func (Callable[..., Any]): A function to generate data for requests. + workflow ([Workflow]): The workflow context to apply to the data generator. + use_case ([BaseUseCase]): The strategy object to construct requests based on the generated data. + Should be a subclass of BaseUseCase. Example - ClinicalDecisionSupport() + """ + self.data_generator_func: Callable[..., Any] = func + self.workflow: Workflow = workflow + self.use_case: BaseUseCase = use_case + self.request_data: List[Dict] = [] + + def generate_request(self, *args: Any, **kwargs: Any) -> None: + """ + Generates a request using the data produced by the data generator function, + and appends it to the internal request queue. + + Parameters: + *args (Any): Positional arguments passed to the data generator function. + **kwargs (Any): Keyword arguments passed to the data generator function. + + Raises: + ValueError: If the use case is not configured. + """ + data = self.data_generator_func(*args, **kwargs) + self.request_data.append(self.use_case.construct_request(data, self.workflow)) + + def send_request(self, url: str) -> List[Dict]: + """ + Sends all queued requests to the specified URL and collects the responses. + + Parameters: + url (str): The URL to which the requests will be sent. + Returns: + List[dict]: A list of JSON responses from the server. + Notes: + This method logs errors rather than raising them, to avoid interrupting the batch processing of requests. + """ + json_responses: List[Dict] = [] + for request in self.request_data: + try: + response = requests.post( + url=url, data=request.model_dump_json(exclude_none=True) + ) + json_responses.append(response.json()) + except Exception as e: + log.error(f"Error sending request: {e}") + json_responses.append({}) + + return json_responses diff --git a/healthchain/decorators.py b/healthchain/decorators.py new file mode 100644 index 0000000..46765cb --- /dev/null +++ b/healthchain/decorators.py @@ -0,0 +1,71 @@ +import logging + +from functools import wraps +from typing import Any, TypeVar, Optional, Callable, Union + +from .base import Workflow, BaseUseCase, UseCaseType +from .clients import EHRClient + +log = logging.getLogger(__name__) + +F = TypeVar("F", bound=Callable) + + +# TODO: add validator and error handling +def ehr( + func: Optional[F] = None, *, workflow: Workflow, num: int = 1 +) -> Union[Callable[..., Any], Callable[[F], F]]: + """ + A decorator that wraps around a data generator function and returns an EHRClient + + Parameters: + func (Optional[Callable]): The function to be decorated. If None, this allows the decorator to + be used with arguments. + workflow ([str]): The workflow identifier which should match an item in the Workflow enum. + This specifies the context in which the EHR function will operate. + num (int): The number of requests to generate in the queue; defaults to 1. + + Returns: + Callable: A decorated callable that incorporates EHR functionality or the decorator itself + if 'func' is None, allowing it to be used as a parameterized decorator. + + Raises: + ValueError: If the workflow does not correspond to any defined enum or if use case is not configured. + NotImplementedError: If the use case class is not one of the supported types. + + Example: + @ehr(workflow='patient-view', num=2) + def generate_data(self, config): + # Function implementation + """ + + def decorator(func: F) -> F: + @wraps(func) + def wrapper(self: BaseUseCase, *args: Any, **kwargs: Any) -> EHRClient: + use_case = getattr(self, "use_case", None) + if use_case is None: + raise ValueError( + f"Use case not configured! Check {type(self)} is a valid strategy." + ) + + try: + workflow_enum = Workflow(workflow) + except ValueError as e: + raise ValueError( + f"{e}: please select from {[x.value for x in Workflow]}" + ) + + if use_case.__class__.__name__ in [e.value for e in UseCaseType]: + method = EHRClient(func, workflow=workflow_enum, use_case=use_case) + for _ in range(num): + method.generate_request(self, *args, **kwargs) + else: + raise NotImplementedError + return method + + return wrapper + + if func is None: + return decorator + else: + return decorator(func) diff --git a/healthchain/integrations/__init__.py b/healthchain/integrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/healthchain/integrations/langchain.py b/healthchain/integrations/langchain.py new file mode 100644 index 0000000..e69de29 diff --git a/healthchain/integrations/spacy.py b/healthchain/integrations/spacy.py new file mode 100644 index 0000000..e69de29 diff --git a/healthchain/models/__init__.py b/healthchain/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/healthchain/models/hooks/__init__.py b/healthchain/models/hooks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/healthchain/models/hooks/basehookcontext.py b/healthchain/models/hooks/basehookcontext.py new file mode 100644 index 0000000..72995a4 --- /dev/null +++ b/healthchain/models/hooks/basehookcontext.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel +from abc import ABC + + +class BaseHookContext(BaseModel, ABC): + userId: str + patientId: str diff --git a/healthchain/models/hooks/encounterdischarge.py b/healthchain/models/hooks/encounterdischarge.py new file mode 100644 index 0000000..acda56f --- /dev/null +++ b/healthchain/models/hooks/encounterdischarge.py @@ -0,0 +1,32 @@ +from pydantic import Field + +from .basehookcontext import BaseHookContext + + +class EncounterDischargeContext(BaseHookContext): + """ + Workflow: + This hook is triggered during the discharge process for typically inpatient encounters. It can be invoked + at any point from the start to the end of the discharge process. The purpose is to allow hook services to + intervene in various aspects of the discharge decision. This includes verifying discharge medications, + ensuring continuity of care planning, and verifying necessary documentation for discharge processing. + + Attributes: + userId (str): REQUIRED. The ID of the current user, expected to be a Practitioner or PractitionerRole. + For example, 'Practitioner/123'. + patientId (str): REQUIRED. The FHIR Patient.id of the patient being discharged. + encounterId (str): REQUIRED. The FHIR Encounter.id of the encounter being ended. + + Documentation: https://cds-hooks.org/hooks/encounter-discharge/ + """ + + userId: str = Field( + ..., + description="The ID of the current user, expected to be in the format 'Practitioner/123'.", + ) + patientId: str = Field( + ..., description="The FHIR Patient.id of the patient being discharged." + ) + encounterId: str = Field( + ..., description="The FHIR Encounter.id of the encounter being ended." + ) diff --git a/healthchain/models/hooks/orderselect.py b/healthchain/models/hooks/orderselect.py new file mode 100644 index 0000000..06e8a58 --- /dev/null +++ b/healthchain/models/hooks/orderselect.py @@ -0,0 +1,59 @@ +from pydantic import Field, model_validator +from typing import List, Dict, Optional, Any +from typing_extensions import Self + +from .basehookcontext import BaseHookContext + + +class OrderSelectContext(BaseHookContext): + """ + Workflow: The order-select hook occurs after the clinician selects the order and before signing. + This hook occurs when a clinician initially selects one or more new orders from a list of + potential orders for a specific patient (including orders for medications, procedures, labs + and other orders). The newly selected order defines that medication, procedure, lab, etc, + but may or may not define the additional details necessary to finalize the order. + + Attributes: + userId (str): REQUIRED. An identifier of the current user, in the format [ResourceType]/[id], + where ResourceType is either 'Practitioner' or 'PractitionerRole'. Examples: 'PractitionerRole/123', + 'Practitioner/abc'. + patientId (str): REQUIRED. The FHIR Patient.id representing the current patient in context. + encounterId (Optional[str]): OPTIONAL. The FHIR Encounter.id representing the current encounter in context, + if applicable. + selections ([str]): REQUIRED. A list of the FHIR id(s) of the newly selected orders, referencing resources + in the draftOrders Bundle. Example: 'MedicationRequest/103'. + draftOrders (object): REQUIRED. A Bundle of FHIR request resources with a draft status, representing all unsigned + orders from the current session, including newly selected orders. + + Documentation: https://cds-hooks.org/hooks/order-select/ + """ + + # TODO: validate selection and FHIR Bundle resource + + userId: str = Field( + pattern=r"^(Practitioner|PractitionerRole)/[^\s]+$", + description="An identifier of the current user in the format [ResourceType]/[id].", + ) + patientId: str = Field( + ..., + description="The FHIR Patient.id representing the current patient in context.", + ) + encounterId: Optional[str] = Field( + None, + description="The FHIR Encounter.id of the current encounter, if applicable.", + ) + selections: List[str] = Field( + ..., description="A list of the FHIR ids of the newly selected orders." + ) + draftOrders: Dict[str, Any] = Field( + ..., description="A Bundle of FHIR request resources with a draft status." + ) + + @model_validator(mode="after") + def validate_selections(self) -> Self: + for selection in self.selections: + if "/" not in selection: + raise ValueError( + "Each selection must be a valid FHIR resource identifier in the format 'ResourceType/ResourceID'." + ) + return self diff --git a/healthchain/models/hooks/ordersign.py b/healthchain/models/hooks/ordersign.py new file mode 100644 index 0000000..aae6f78 --- /dev/null +++ b/healthchain/models/hooks/ordersign.py @@ -0,0 +1,44 @@ +from pydantic import Field +from typing import Optional, Dict, Any + +from .basehookcontext import BaseHookContext + + +class OrderSignContext(BaseHookContext): + """ + Workflow: + The order-sign hook is triggered when a clinician is ready to sign one or more orders for a patient. + This includes orders for medications, procedures, labs, and other orders. It is one of the last workflow + events before an order is promoted from a draft status. The context includes all order details such as + dose, quantity, route, etc., even though the order is still in a draft status. This hook is also applicable + for re-signing revised orders, which may have a status other than 'draft'. The hook replaces the + medication-prescribe and order-review hooks. + + Attributes: + userId (str): REQUIRED. The ID of the current user, expected to be of type 'Practitioner' or 'PractitionerRole'. + Examples include 'PractitionerRole/123' or 'Practitioner/abc'. + patientId (str): REQUIRED. The FHIR Patient.id representing the current patient in context. + encounterId (Optional[str]): OPTIONAL. The FHIR Encounter.id of the current encounter in context. + draftOrders (dict): REQUIRED. A Bundle of FHIR request resources with a draft status, representing orders that + aren't yet signed from the current ordering session. + + Documentation: https://cds-hooks.org/hooks/order-sign/ + """ + + # TODO: validate draftOrders + + userId: str = Field( + pattern=r"^(Practitioner|PractitionerRole)/[^\s]+$", + description="The ID of the current user in the format [ResourceType]/[id].", + ) + patientId: str = Field( + ..., + description="The FHIR Patient.id representing the current patient in context.", + ) + encounterId: Optional[str] = Field( + None, + description="The FHIR Encounter.id of the current encounter, if applicable.", + ) + draftOrders: Dict[str, Any] = Field( + ..., description="A Bundle of FHIR request resources with a draft status." + ) diff --git a/healthchain/models/hooks/patientview.py b/healthchain/models/hooks/patientview.py new file mode 100644 index 0000000..5456769 --- /dev/null +++ b/healthchain/models/hooks/patientview.py @@ -0,0 +1,32 @@ +from pydantic import Field +from typing import Optional + +from .basehookcontext import BaseHookContext + + +class PatientViewContext(BaseHookContext): + """ + Workflow: The user has just opened a patient's record; typically called only once at the beginning of a user's + interaction with a specific patient's record. + + Attributes: + userId (str): An identifier of the current user, in the format [ResourceType]/[id], + where ResourceType is one of 'Practitioner', 'PractitionerRole', 'Patient', + or 'RelatedPerson'. Examples: 'Practitioner/abc', 'Patient/123'. + patientId (str): The FHIR Patient.id representing the current patient in context. + encounterId (Optional[str]): The FHIR Encounter.id representing the current encounter in context, + if applicable. This field is optional. + + Documentation: https://cds-hooks.org/hooks/patient-view/ + """ + + # TODO: more comprehensive validator? for now regex should suffice + + userId: str = Field( + pattern=r"^(Practitioner|PractitionerRole|Patient|RelatedPerson)/[^\s]+$", + description="The ID of the current user, expected to be in the format 'Practitioner/123'.", + ) + patientId: str = Field(..., description="The FHIR Patient.id of the patient.") + encounterId: Optional[str] = Field( + None, description="The FHIR Encounter.id of the encounter, if applicable." + ) diff --git a/healthchain/models/hooks/sign-note-inpatient.py b/healthchain/models/hooks/sign-note-inpatient.py new file mode 100644 index 0000000..e69de29 diff --git a/healthchain/models/hooks/sign-note-outpatient.py b/healthchain/models/hooks/sign-note-outpatient.py new file mode 100644 index 0000000..e69de29 diff --git a/healthchain/models/requests/__init__.py b/healthchain/models/requests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/healthchain/models/requests/cdsrequest.py b/healthchain/models/requests/cdsrequest.py new file mode 100644 index 0000000..4e52687 --- /dev/null +++ b/healthchain/models/requests/cdsrequest.py @@ -0,0 +1,43 @@ +from pydantic import BaseModel, HttpUrl +from typing import Optional, List, Dict, Any + +from ..hooks.basehookcontext import BaseHookContext + +# TODO: add docstrings + + +class FHIRAuthorization(BaseModel): + access_token: str # OAuth2 access token + token_type: str = "Bearer" + expires_in: int + scope: str + subject: str + + +class CDSRequest(BaseModel): + """ + A model representing the data structure for a CDS service call, triggered by specific hooks + within a healthcare application. + + Attributes: + hook (str): The hook that triggered this CDS Service call. For example, 'patient-view'. + hookInstance (UUID): A universally unique identifier for this particular hook call. + fhirServer (HttpUrl): The base URL of the CDS Client's FHIR server. This field is required if `fhirAuthorization` is provided. + fhirAuthorization (Optional[FhirAuthorization]): Optional authorization details providing a bearer access token for FHIR resources. + context (Dict[str, Any]): Hook-specific contextual data required by the CDS service. + prefetch (Optional[Dict[str, Any]]): Optional FHIR data that was prefetched by the CDS Client. + + Documentation: https://cds-hooks.org/specification/current/#http-request_1 + """ + + hook: str + hookInstance: str + context: BaseHookContext + fhirServer: Optional[HttpUrl] = None + fhirAuthorization: Optional[FHIRAuthorization] = ( + None # TODO: note this is required if fhirserver is given + ) + prefetch: Optional[Dict[str, Any]] = ( + None # fhir resource is passed either thru prefetched template of fhir server + ) + extension: Optional[List[Dict[str, Any]]] = None diff --git a/healthchain/models/responses/__init__.py b/healthchain/models/responses/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/healthchain/models/responses/cdsdiscovery.py b/healthchain/models/responses/cdsdiscovery.py new file mode 100644 index 0000000..590b19c --- /dev/null +++ b/healthchain/models/responses/cdsdiscovery.py @@ -0,0 +1,39 @@ +""" +https://cds-hooks.org/specification/current/#discovery +""" + +from pydantic import BaseModel +from typing import Optional, List, Dict, Any + + +class CDSService(BaseModel): + """ + A model representing a CDS service configuration. + + Attributes: + hook (str): The hook this service should be invoked on. This should correspond to one of the predefined hooks. + title (Optional[str]): The human-friendly name of this service. It is recommended to provide this for better usability. + description (str): A detailed description of what this service does and its purpose within the CDS framework. + id (str): The unique identifier of this service. It forms part of the URL as {baseUrl}/cds-services/{id}. + prefetch (Optional[Dict[str, str]]): Optional FHIR queries that the service requests the CDS Client to perform + and provide on each service call. Keys describe the type of data and values are the actual FHIR query strings. + usageRequirements (Optional[str]): Human-friendly description of any preconditions for the use of this CDS service. + + Documentation: https://cds-hooks.org/specification/current/#response + """ + + hook: str + description: str + id: str + title: Optional[str] + prefetch: Optional[Dict[str, Any]] = None + usageRequirements: Optional[str] = None + + +class CDSServiceInformation(BaseModel): + """ + A CDS Service is discoverable via a stable endpoint by CDS Clients. The Discovery endpoint includes information such as a + description of the CDS Service, when it should be invoked, and any data that is requested to be prefetched. + """ + + services: List[CDSService] diff --git a/healthchain/models/responses/cdsfeedback.py b/healthchain/models/responses/cdsfeedback.py new file mode 100644 index 0000000..29f748d --- /dev/null +++ b/healthchain/models/responses/cdsfeedback.py @@ -0,0 +1,43 @@ +""" +This is not compulsary + +https://cds-hooks.org/specification/current/#feedback +""" + +from pydantic import BaseModel +from typing import Optional, Dict, Any +from enum import Enum + +from .cdsresponse import Coding + + +class OutcomeEnum(str, Enum): + accepted = "accepted" + overridden = "overridden" + + +class OverrideReason(BaseModel): + reason: Coding + userComment: Optional[str] = None + + +class CDSFeedback(BaseModel): + """ + A feedback endpoint enables suggestion tracking & analytics. + A CDS Service MAY support a feedback endpoint; a CDS Client SHOULD be capable of sending feedback. + + Attributes: + card (str): The card.uuid from the CDS Hooks response. Uniquely identifies the card. + outcome (str): The outcome of the action, either 'accepted' or 'overridden'. + acceptedSuggestions (List[AcceptedSuggestion]): An array of accepted suggestions, required if the outcome is 'accepted'. + overrideReason (Optional[OverrideReason]): The reason for overriding, including any coding and comments. + outcomeTimestamp (datetime): The ISO8601 timestamp of when the action was taken on the card. + + Documentation: https://cds-hooks.org/specification/current/#feedback + """ + + card: str + outcome: OutcomeEnum + outcomeTimestamp: str + acceptedSuggestion: Optional[Dict[str, Any]] = None + overriddeReason: Optional[OverrideReason] = None diff --git a/healthchain/models/responses/cdsresponse.py b/healthchain/models/responses/cdsresponse.py new file mode 100644 index 0000000..f3e2ab5 --- /dev/null +++ b/healthchain/models/responses/cdsresponse.py @@ -0,0 +1,192 @@ +from enum import Enum + +from pydantic import BaseModel, Field, HttpUrl, field_validator, model_validator +from typing import Optional, List, Dict +from typing_extensions import Self + + +class IndicatorEnum(str, Enum): + """ + Urgency/importance of what Card conveys. + Allowed values, in order of increasing urgency, are: info, warning, critical. + The CDS Client MAY use this field to help make UI display decisions such as sort order or coloring. + """ + + info = "info" + warning = "warning" + critical = "critical" + + +class SelectionBehaviorEnum(str, Enum): + """ + Describes the intended selection behavior of the suggestions in the card. + Allowed values are: at-most-one, indicating that the user may choose none or + at most one of the suggestions; any, indicating that the end user may choose + any number of suggestions including none of them and all of them. + CDS Clients that do not understand the value MUST treat the card as an error. + """ + + at_most_one = "at-most-one" + any = "any" + + +class ActionTypeEnum(str, Enum): + """ + The type of action being performed + """ + + create = "create" + update = "update" + delete = "delete" + + +class LinkTypeEnum(str, Enum): + """ + The type of the given URL. There are two possible values for this field. + A type of absolute indicates that the URL is absolute and should be treated as-is. + A type of smart indicates that the URL is a SMART app launch URL and the CDS Client + should ensure the SMART app launch URL is populated with the appropriate SMART + launch parameters. + """ + + absolute = "absolute" + smart = "smart" + + +class Link(BaseModel): + """ + * CDS Client support for appContext requires additional coordination with the authorization + server that is not described or specified in CDS Hooks nor SMART. + + * Autolaunchable is experimental + + https://cds-hooks.org/specification/current/#link + """ + + label: str + url: HttpUrl + type: LinkTypeEnum + appContext: Optional[str] = None + autoLaunchable: Optional[bool] + + @model_validator(mode="after") + def validate_link(self) -> Self: + if self.appContext: + assert ( + self.type == LinkTypeEnum.smart + ), "'type' must be 'smart' for appContext to be valued." + + return self + + +class Coding(BaseModel): + """ + The Coding data type captures the concept of a code. This coding type is a standalone data type + in CDS Hooks modeled after a trimmed down version of the FHIR Coding data type. + """ + + code: str + system: str + display: Optional[str] = None + + +class Action(BaseModel): + """ + Within a suggestion, all actions are logically AND'd together, such that a user selecting a + suggestion selects all of the actions within it. When a suggestion contains multiple actions, + the actions SHOULD be processed as per FHIR's rules for processing transactions with the CDS + Client's fhirServer as the base url for the inferred full URL of the transaction bundle entries. + + https://cds-hooks.org/specification/current/#action + """ + + type: ActionTypeEnum + description: str + resource: Optional[Dict] = None + resourceId: Optional[str] = None + + @model_validator(mode="after") + def validate_action_type(self) -> Self: + if self.type in [ActionTypeEnum.create, ActionTypeEnum.update]: + assert ( + self.resource + ), f"'resource' must be provided when type is '{self.type.value}'" + else: + assert ( + self.resourceId + ), f"'resourceId' must be provided when type is '{self.type.value}'" + + return self + + +class Suggestion(BaseModel): + """ + Allows a service to suggest a set of changes in the context of the current activity + (e.g. changing the dose of a medication currently being prescribed, for the order-sign activity). + If suggestions are present, selectionBehavior MUST also be provided. + + https://cds-hooks.org/specification/current/#suggestion + """ + + label: str + uuid: Optional[str] = None + isRecommended: Optional[bool] + actions: Optional[List[Action]] = [] + + +class Source(BaseModel): + """ + Grouping structure for the Source of the information displayed on this card. + The source should be the primary source of guidance for the decision support Card represents. + + https://cds-hooks.org/specification/current/#source + """ + + label: str + url: Optional[HttpUrl] = None + icon: Optional[HttpUrl] = None + topic: Optional[Coding] = None + + +class Card(BaseModel): + """ + Cards can provide a combination of information (for reading), suggested actions + (to be applied if a user selects them), and links (to launch an app if the user selects them). + The CDS Client decides how to display cards, but this specification recommends displaying suggestions + using buttons, and links using underlined text. + + https://cds-hooks.org/specification/current/#card-attributes + """ + + summary: str = Field(..., max_length=140) + indicator: IndicatorEnum + source: Source + uuid: Optional[str] = None + detail: Optional[str] = None + suggestions: Optional[List[Suggestion]] = None + selectionBehavior: Optional[SelectionBehaviorEnum] = None + overrideReasons: Optional[List[Coding]] = None + links: Optional[List[Link]] = None + + @model_validator(mode="after") + def validate_suggestions(self) -> Self: + if self.suggestions is not None: + assert self.selectionBehavior, f"'selectionBehavior' must be given if 'suggestions' is present! Choose from {[v for v in SelectionBehaviorEnum.value]}" + return self + + @field_validator("detail") + @classmethod + def validate_markdown(cls, v: str) -> str: + if v is not None: + if not (v.startswith("#") or v.startswith("*")): + raise ValueError("Detail must be in GitHub Flavored Markdown format.") + return v + + +class CDSResponse(BaseModel): + """ + Http response + """ + + cards: List[Card] = [] + systemActions: Optional[Action] = None diff --git a/healthchain/service/__init__.py b/healthchain/service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/healthchain/service/server.py b/healthchain/service/server.py new file mode 100644 index 0000000..83656f3 --- /dev/null +++ b/healthchain/service/server.py @@ -0,0 +1,3 @@ +""" +The model needs to be wrapped in a REST API server spcified by CDS / Epic etc. +""" diff --git a/healthchain/service/soap.py b/healthchain/service/soap.py new file mode 100644 index 0000000..e69de29 diff --git a/healthchain/service/specifications/__init__.py b/healthchain/service/specifications/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/healthchain/service/specifications/epic-notereader-service.py b/healthchain/service/specifications/epic-notereader-service.py new file mode 100644 index 0000000..e69de29 diff --git a/healthchain/service/specifications/hl7-cds-service.py b/healthchain/service/specifications/hl7-cds-service.py new file mode 100644 index 0000000..e69de29 diff --git a/healthchain/use_cases/__init__.py b/healthchain/use_cases/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/healthchain/use_cases/cds.py b/healthchain/use_cases/cds.py new file mode 100644 index 0000000..5c87cea --- /dev/null +++ b/healthchain/use_cases/cds.py @@ -0,0 +1,69 @@ +import logging + +from typing import Dict + +from ..base import BaseUseCase, UseCaseMapping, Workflow, validate_workflow +from ..models.requests.cdsrequest import CDSRequest +from ..models.hooks.orderselect import OrderSelectContext +from ..models.hooks.ordersign import OrderSignContext +from ..models.hooks.patientview import PatientViewContext +from ..models.hooks.encounterdischarge import EncounterDischargeContext + +log = logging.getLogger(__name__) + + +class ClinicalDecisionSupport(BaseUseCase): + """ + Implements EHR backend strategy for Clinical Decision Support (CDS) + """ + + def __init__(self) -> None: + super().__init__() + self.context_mapping = { + Workflow.order_select: OrderSelectContext, + Workflow.order_sign: OrderSignContext, + Workflow.patient_view: PatientViewContext, + Workflow.encounter_discharge: EncounterDischargeContext, + } + + @property + def description(self) -> str: + return "Clinical decision support (HL7 CDS specification)" + + def _validate_data(self, data, workflow: Workflow) -> bool: + # do something to valida fhir data and the worklow it's for + return True + + @validate_workflow(UseCaseMapping.ClinicalDecisionSupport) + def construct_request(self, data, workflow: Workflow) -> Dict: + """ + Constructs a HL7-compliant CDS request based on workflow. + + Parameters: + data: FHIR data to be injected in request. + workflow (Workflow): The CDS hook name, e.g. patient-view. + + Returns: + Dict: A json-compatible CDS request. + + Raises: + ValueError: If the workflow is invalid or the data does not validate properly. + """ + # TODO: sub data for actual DoppelData format!! + if self._validate_data(data, workflow): + log.debug(f"Constructing CDS request for {workflow.value} from {data}") + + context_model = self.context_mapping.get(workflow, None) + if context_model is None: + raise ValueError( + f"Invalid workflow {workflow.value} or workflow model not implemented." + ) + + context = context_model(**data.context) + request = CDSRequest( + hook=workflow.value, hookInstance=data.uuid, context=context + ) + else: + raise ValueError(f"Error validating data for workflow {Workflow}") + + return request diff --git a/healthchain/use_cases/clindoc.py b/healthchain/use_cases/clindoc.py new file mode 100644 index 0000000..7b2038b --- /dev/null +++ b/healthchain/use_cases/clindoc.py @@ -0,0 +1,33 @@ +import logging + +from typing import Dict + +from ..base import BaseUseCase, UseCaseMapping, Workflow, validate_workflow + +log = logging.getLogger(__name__) + + +# TODO: TO IMPLEMENT +class ClinicalDocumentation(BaseUseCase): + """ + Implements EHR backend strategy for clinical documentation (NoteReader) + """ + + @property + def description(self) -> str: + return "Clinical documentation (NoteReader)" + + def _validate_data(self, data, workflow: Workflow) -> bool: + # do something to validate cda data and the workflow it's for + return True + + @validate_workflow(UseCaseMapping.ClinicalDocumentation) + def construct_request(self, data, workflow: Workflow) -> Dict: + if self._validate_data(data, workflow): + # do something to construct a notereader soap request + log.debug("Constructing Clinical Documentation request...") + request = {} + else: + raise ValueError(f"Error validating data for workflow {Workflow}") + + return request diff --git a/healthchain/utils/__init__.py b/healthchain/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/healthchain/utils/logger.py b/healthchain/utils/logger.py new file mode 100644 index 0000000..edebc23 --- /dev/null +++ b/healthchain/utils/logger.py @@ -0,0 +1,35 @@ +import logging +from colorama import init, Fore, Back + +init(autoreset=True) + + +class ColorFormatter(logging.Formatter): + # Change this dictionary to suit your coloring needs! + COLORS = { + "WARNING": Fore.YELLOW, + "ERROR": Fore.RED, + "DEBUG": Fore.BLUE, + "INFO": Fore.GREEN, + "CRITICAL": Fore.RED + Back.WHITE, + } + + def format(self, record): + color = self.COLORS.get(record.levelname, "") + if color: + record.asctime = color + self.formatTime(record, self.datefmt) + record.name = color + record.name + record.levelname = color + record.levelname + record.msg = color + record.msg + return logging.Formatter.format(self, record) + + +class ColorLogger(logging.Logger): + def __init__(self, name): + logging.Logger.__init__(self, name, logging.DEBUG) + color_formatter = ColorFormatter( + "%(asctime)-10s %(levelname)s [%(module)s]: %(message)s" + ) + console = logging.StreamHandler() + console.setFormatter(color_formatter) + self.addHandler(console) diff --git a/mkdocs.yml b/mkdocs.yml index 211afff..14fde43 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -15,4 +15,4 @@ theme: tabs: true palette: primary: white - accent: blue \ No newline at end of file + accent: blue diff --git a/poetry.lock b/poetry.lock index 49ff557..594f5f8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,137 @@ # This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + +[[package]] +name = "certifi" +version = "2024.2.2" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + [[package]] name = "click" version = "8.1.7" @@ -25,6 +157,33 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[[package]] +name = "filelock" +version = "3.14.0" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"}, + {file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] + [[package]] name = "ghp-import" version = "2.1.0" @@ -42,6 +201,31 @@ python-dateutil = ">=2.8.1" [package.extras] dev = ["flake8", "markdown", "twine", "wheel"] +[[package]] +name = "identify" +version = "2.5.36" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, + {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.7" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + [[package]] name = "iniconfig" version = "2.0.0" @@ -211,6 +395,20 @@ mergedeep = ">=1.3.4" platformdirs = ">=2.2.0" pyyaml = ">=5.1" +[[package]] +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[package.dependencies] +setuptools = "*" + [[package]] name = "packaging" version = "24.0" @@ -263,25 +461,153 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pre-commit" +version = "3.7.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.7.0-py2.py3-none-any.whl", hash = "sha256:5eae9e10c2b5ac51577c3452ec0a490455c45a0533f7960f993a0d01e59decab"}, + {file = "pre_commit-3.7.0.tar.gz", hash = "sha256:e209d61b8acdcf742404408531f0c37d49d2c734fd7cff2d6076083d191cb060"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pydantic" +version = "2.7.1" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.7.1-py3-none-any.whl", hash = "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5"}, + {file = "pydantic-2.7.1.tar.gz", hash = "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.18.2" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.18.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.18.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81"}, + {file = "pydantic_core-2.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857"}, + {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563"}, + {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38"}, + {file = "pydantic_core-2.18.2-cp310-none-win32.whl", hash = "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027"}, + {file = "pydantic_core-2.18.2-cp310-none-win_amd64.whl", hash = "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543"}, + {file = "pydantic_core-2.18.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3"}, + {file = "pydantic_core-2.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c"}, + {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0"}, + {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664"}, + {file = "pydantic_core-2.18.2-cp311-none-win32.whl", hash = "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e"}, + {file = "pydantic_core-2.18.2-cp311-none-win_amd64.whl", hash = "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3"}, + {file = "pydantic_core-2.18.2-cp311-none-win_arm64.whl", hash = "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d"}, + {file = "pydantic_core-2.18.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242"}, + {file = "pydantic_core-2.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c"}, + {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241"}, + {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3"}, + {file = "pydantic_core-2.18.2-cp312-none-win32.whl", hash = "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038"}, + {file = "pydantic_core-2.18.2-cp312-none-win_amd64.whl", hash = "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438"}, + {file = "pydantic_core-2.18.2-cp312-none-win_arm64.whl", hash = "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec"}, + {file = "pydantic_core-2.18.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439"}, + {file = "pydantic_core-2.18.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b"}, + {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761"}, + {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788"}, + {file = "pydantic_core-2.18.2-cp38-none-win32.whl", hash = "sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350"}, + {file = "pydantic_core-2.18.2-cp38-none-win_amd64.whl", hash = "sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e"}, + {file = "pydantic_core-2.18.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8"}, + {file = "pydantic_core-2.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4"}, + {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399"}, + {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b"}, + {file = "pydantic_core-2.18.2-cp39-none-win32.whl", hash = "sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e"}, + {file = "pydantic_core-2.18.2-cp39-none-win_amd64.whl", hash = "sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374"}, + {file = "pydantic_core-2.18.2.tar.gz", hash = "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "pytest" -version = "8.1.1" +version = "8.2.0" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, - {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, + {file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"}, + {file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.4,<2.0" +pluggy = ">=1.5,<2.0" [package.extras] -testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "python-dateutil" @@ -322,6 +648,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -370,6 +697,69 @@ files = [ [package.dependencies] pyyaml = "*" +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "ruff" +version = "0.4.2" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8d14dc8953f8af7e003a485ef560bbefa5f8cc1ad994eebb5b12136049bbccc5"}, + {file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:24016ed18db3dc9786af103ff49c03bdf408ea253f3cb9e3638f39ac9cf2d483"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2e06459042ac841ed510196c350ba35a9b24a643e23db60d79b2db92af0c2b"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3afabaf7ba8e9c485a14ad8f4122feff6b2b93cc53cd4dad2fd24ae35112d5c5"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:799eb468ea6bc54b95527143a4ceaf970d5aa3613050c6cff54c85fda3fde480"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ec4ba9436a51527fb6931a8839af4c36a5481f8c19e8f5e42c2f7ad3a49f5069"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6a2243f8f434e487c2a010c7252150b1fdf019035130f41b77626f5655c9ca22"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8772130a063f3eebdf7095da00c0b9898bd1774c43b336272c3e98667d4fb8fa"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ab165ef5d72392b4ebb85a8b0fbd321f69832a632e07a74794c0e598e7a8376"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1f32cadf44c2020e75e0c56c3408ed1d32c024766bd41aedef92aa3ca28eef68"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:22e306bf15e09af45ca812bc42fa59b628646fa7c26072555f278994890bc7ac"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82986bb77ad83a1719c90b9528a9dd663c9206f7c0ab69282af8223566a0c34e"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:652e4ba553e421a6dc2a6d4868bc3b3881311702633eb3672f9f244ded8908cd"}, + {file = "ruff-0.4.2-py3-none-win32.whl", hash = "sha256:7891ee376770ac094da3ad40c116258a381b86c7352552788377c6eb16d784fe"}, + {file = "ruff-0.4.2-py3-none-win_amd64.whl", hash = "sha256:5ec481661fb2fd88a5d6cf1f83403d388ec90f9daaa36e40e2c003de66751798"}, + {file = "ruff-0.4.2-py3-none-win_arm64.whl", hash = "sha256:cbd1e87c71bca14792948c4ccb51ee61c3296e164019d2d484f3eaa2d360dfaf"}, + {file = "ruff-0.4.2.tar.gz", hash = "sha256:33bcc160aee2520664bc0859cfeaebc84bb7323becff3f303b8f1f2d81cb4edc"}, +] + +[[package]] +name = "setuptools" +version = "69.5.1" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"}, + {file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + [[package]] name = "six" version = "1.16.0" @@ -381,6 +771,54 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "typing-extensions" +version = "4.11.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, +] + +[[package]] +name = "urllib3" +version = "2.2.1" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "virtualenv" +version = "20.26.1" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.26.1-py3-none-any.whl", hash = "sha256:7aa9982a728ae5892558bff6a2839c00b9ed145523ece2274fad6f414690ae75"}, + {file = "virtualenv-20.26.1.tar.gz", hash = "sha256:604bfdceaeece392802e6ae48e69cec49168b9c5f4a44e483963f9242eb0e78b"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + [[package]] name = "watchdog" version = "4.0.0" @@ -425,4 +863,4 @@ watchmedo = ["PyYAML (>=3.10)"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "35d94ccf245203340acb12038e5dec577e25ffbc71da5e36c9d7026225130cc5" +content-hash = "0b425e65b14697aadd7680ae01bef0d2dbc1c3bf307681b1f8ef41f34d84d817" diff --git a/pyproject.toml b/pyproject.toml index b52b114..f80a963 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,17 +1,22 @@ [tool.poetry] -name = "doppeldata" +name = "HealthChain" version = "0.1.0" -description = "" -authors = ["Adam Kells "] +description = "Remarkably simple LLM prototyping and testing in healthcare context." +authors = ["Adam Kells ", "Jennifer Jiang-Kells "] license = "MIT" readme = "README.md" [tool.poetry.dependencies] python = "^3.11" mkdocs = "^1.5.3" +pydantic = "^2.7.1" +requests = "^2.31.0" +colorama = "^0.4.6" [tool.poetry.group.dev.dependencies] -pytest = "^8.1.1" +ruff = "^0.4.2" +pytest = "^8.2.0" +pre-commit = "^3.7.0" [build-system] requires = ["poetry-core"] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_clients.py b/tests/test_clients.py new file mode 100644 index 0000000..34e02ac --- /dev/null +++ b/tests/test_clients.py @@ -0,0 +1,68 @@ +import pytest +from unittest.mock import Mock, patch +from healthchain.clients import EHRClient + + +@pytest.fixture +def mock_function(): + return Mock() + + +@pytest.fixture +def mock_workflow(): + return Mock() + + +@pytest.fixture +def mock_use_case(): + mock = Mock() + mock.construct_request = Mock( + return_value=Mock(model_dump_json=Mock(return_value="{}")) + ) + return mock + + +@pytest.fixture +def ehr_client(mock_function, mock_workflow, mock_use_case): + return EHRClient(mock_function, mock_workflow, mock_use_case) + + +def test_init(ehr_client, mock_function, mock_workflow, mock_use_case): + assert ehr_client.data_generator_func == mock_function + assert ehr_client.workflow == mock_workflow + assert ehr_client.use_case == mock_use_case + assert ehr_client.request_data == [] + + +def test_generate_request(ehr_client, mock_use_case): + ehr_client.generate_request(1, 2, test="data") + mock_use_case.construct_request.assert_called_once() + assert len(ehr_client.request_data) == 1 + + +@patch("requests.post") +def test_send_request(mock_post, ehr_client): + # Configure the mock to return a successful response + mock_post.return_value.json = Mock(return_value={"status": "success"}) + mock_post.return_value.status_code = 200 + ehr_client.request_data = [ + Mock(model_dump_json=Mock(return_value="{}")) for _ in range(2) + ] + + responses = ehr_client.send_request("http://fakeurl.com") + + assert mock_post.call_count == 2 + assert all(response["status"] == "success" for response in responses) + + # Test error handling + mock_post.side_effect = Exception("Failed to connect") + responses = ehr_client.send_request("http://fakeurl.com") + assert {} in responses # Check if empty dict was appended due to the error + + +def test_logging_on_send_request_error(caplog, ehr_client): + with patch("requests.post") as mock_post: + mock_post.side_effect = Exception("Failed to connect") + ehr_client.request_data = [Mock(model_dump_json=Mock(return_value="{}"))] + ehr_client.send_request("http://fakeurl.com") + assert "Error sending request: Failed to connect" in caplog.text diff --git a/tests/test_decorators.py b/tests/test_decorators.py new file mode 100644 index 0000000..c023900 --- /dev/null +++ b/tests/test_decorators.py @@ -0,0 +1,62 @@ +import pytest + +from unittest.mock import Mock +from healthchain.decorators import ehr + + +class MockUseCase: + pass + + +class ClinicalDocumentation: + construct_request = Mock(return_value=Mock(model_dump_json=Mock(return_value="{}"))) + + +class ClinicalDecisionSupport: + construct_request = Mock(return_value=Mock(model_dump_json=Mock(return_value="{}"))) + + +@pytest.fixture +def function(): + def func(self): + pass + + return func + + +class TestEHRDecorator: + def test_use_case_not_configured(self, function): + instance = MockUseCase() + decorated = ehr(workflow="any_workflow")(function) + with pytest.raises(ValueError) as excinfo: + decorated(instance) + assert "Use case not configured" in str(excinfo.value) + + def test_invalid_workflow(self, function): + instance = ClinicalDocumentation() + instance.use_case = ClinicalDocumentation() + with pytest.raises(ValueError) as excinfo: + decorated = ehr(workflow="invalid_workflow")(function) + decorated(instance) + assert "please select from" in str(excinfo.value) + + def test_unsupported_use_case(self, function): + instance = MockUseCase() + instance.use_case = MockUseCase() # This use case should not be supported + decorated = ehr(workflow="patient-view")(function) + with pytest.raises(NotImplementedError): + decorated(instance) + + def test_correct_behavior(self, function): + instance = ClinicalDocumentation() + instance.use_case = ClinicalDocumentation() + decorated = ehr(workflow="order-sign")(function) + result = decorated(instance) + assert len(result.request_data) == 1 + + def test_multiple_calls(self, function): + instance = ClinicalDecisionSupport() + instance.use_case = ClinicalDecisionSupport() + decorated = ehr(workflow="order-select", num=3)(function) + result = decorated(instance) + assert len(result.request_data) == 3 diff --git a/tests/test_usecases.py b/tests/test_usecases.py new file mode 100644 index 0000000..03fb4b9 --- /dev/null +++ b/tests/test_usecases.py @@ -0,0 +1,85 @@ +import pytest +import dataclasses + +from unittest.mock import patch, MagicMock +from healthchain.base import Workflow +from healthchain.models.requests.cdsrequest import CDSRequest +from healthchain.use_cases.cds import ClinicalDecisionSupport +from healthchain.models.hooks.patientview import PatientViewContext + + +@dataclasses.dataclass +class synth_data: + context: dict + uuid: str + prefetch: dict + + +@pytest.fixture +def cds(): + return ClinicalDecisionSupport() + + +@pytest.fixture +def valid_data(): + return synth_data( + context={"userId": "Practitioner/123", "patientId": "123"}, + uuid="1234-5678", + prefetch={}, + ) + + +@pytest.fixture +def invalid_data(): + return synth_data( + context={"invalidId": "Practitioner/123", "patientId": "123"}, + uuid="1234-5678", + prefetch={}, + ) + + +def test_valid_data_request_construction(cds, valid_data): + with patch.object(CDSRequest, "__init__", return_value=None) as mock_init: + cds.construct_request(valid_data, Workflow.patient_view) + mock_init.assert_called_once_with( + hook=Workflow.patient_view.value, + hookInstance="1234-5678", + context=PatientViewContext(userId="Practitioner/123", patientId="123"), + ) + + +# def test_invalid_data_raises_error(cds, invalid_data): +# with pytest.raises(ValueError): +# cds.construct_request(invalid_data, Workflow.patient_view) + + +def test_context_mapping(cds, valid_data): + with patch.dict( + cds.context_mapping, + { + Workflow.patient_view: MagicMock( + spec=PatientViewContext, + return_value=PatientViewContext( + userId="Practitioner/123", patientId="123" + ), + ) + }, + ): + cds.construct_request(data=valid_data, workflow=Workflow.patient_view) + cds.context_mapping[Workflow.patient_view].assert_called_once_with( + **valid_data.context + ) + + +def test_workflow_validation_decorator(cds, valid_data): + with pytest.raises(ValueError) as excinfo: + cds.construct_request(Workflow.notereader_sign_inpatient, valid_data) + assert "Invalid workflow" in str(excinfo.value) + + with pytest.raises(ValueError) as excinfo: + cds.construct_request( + data=valid_data, workflow=Workflow.notereader_sign_inpatient + ) + assert "Invalid workflow" in str(excinfo.value) + + assert cds.construct_request(valid_data, Workflow.patient_view)