diff --git a/README.md b/README.md index c3d6261..f27f61a 100644 --- a/README.md +++ b/README.md @@ -82,8 +82,7 @@ To use the `fq` command, you need to provide the necessary options. Below is an ```sh fq --fhir-base-url \ --graph-definition-id \ - --start-resource-type \ - --start-resource-id \ + --path \ [--graph-definition-file-path ] \ [--db_path ] \ [--debug] diff --git a/fhir_query/__init__.py b/fhir_query/__init__.py index 1ac96c9..a043d95 100644 --- a/fhir_query/__init__.py +++ b/fhir_query/__init__.py @@ -419,14 +419,14 @@ def __init__(self, fhir_base_url: str): """ self.fhir_base_url = fhir_base_url - async def fetch_resource(self, resource_type: str, spinner: Halo = None) -> list[dict[str, Any]]: + async def fetch_resource(self, resource_type: str, spinner: Halo = None) -> dict[str, dict[Any, Any]]: """ Fetch resources of a given type from the FHIR server. :param spinner: A Halo spinner object to show progress. :param resource_type: The type of resource to fetch. :return: A list of resources. """ - counts = {resource_type: {}} + counts: dict = {resource_type: {}} category_counts = counts[resource_type] # A client with a 60s timeout for connecting, and a 10s timeout elsewhere. timeout = httpx.Timeout(10.0, connect=60.0) @@ -464,7 +464,7 @@ async def fetch_resource(self, resource_type: str, spinner: Halo = None) -> list url = next_link return counts - async def collect(self, resource_types: list[str], spinner: Halo = None) -> dict: + async def collect(self, resource_types: list[str], spinner: Halo = None) -> list: """ Collect vocabularies from the specified resource types. :param spinner: A Halo spinner object to show progress. diff --git a/fhir_query/cli.py b/fhir_query/cli.py index a4f92bc..e25b5c5 100644 --- a/fhir_query/cli.py +++ b/fhir_query/cli.py @@ -110,7 +110,7 @@ def vocabularies( if fhir_base_url.endswith("/"): fhir_base_url = fhir_base_url[:-1] - async def collect_vocabularies(_runner: VocabularyRunner, _spinner: Halo) -> dict: + async def collect_vocabularies(_runner: VocabularyRunner, _spinner: Halo) -> list: _counts = await _runner.collect( resource_types=["Observation", "Condition", "Procedure", "Medication", "Specimen", "Encounter", "DocumentReference"], spinner=_spinner, diff --git a/fhir_query/dataframer.py b/fhir_query/dataframer.py index 7f812a0..d03a190 100644 --- a/fhir_query/dataframer.py +++ b/fhir_query/dataframer.py @@ -460,7 +460,7 @@ def get_resources_by_reference(self, resource_type: str, reference_field: str, r resource = json.loads(raw_resource) # determine which how to process the field - if reference_field == "focus": + if reference_field == "focus" and "focus" in resource: # add the resource (eg observation) for each focus reference to the dict for i in range(len(resource["focus"])): reference_key = get_nested_value(resource, [reference_field, i, "reference"]) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index f2e4522..fb44f5d 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -14,6 +14,23 @@ def mock_fhir_server(httpx_mock: HTTPXMock) -> Generator[HTTPXMock, Any, Any]: def dummy_callback(request: httpx.Request) -> Response: logging.warning(f"Request: {request.url.path}, {str(request.url.params)}") + + if request.url.path == "/ResearchStudy" and str(request.url.params) == "_id=123": + return Response( + 200, json={"resourceType": "Bundle", "entry": [{"resource": {"resourceType": "ResearchStudy", "id": "123"}}]} + ) + + if request.url.path == "/ResearchSubject": + return Response( + 200, + json={ + "resourceType": "Bundle", + "entry": [ + {"resource": {"resourceType": "ResearchSubject", "id": "123RS", "subject": {"reference": "Patient/123"}}}, + ], + }, + ) + if request.url.path == "/Patient/123": return Response(200, json={"resourceType": "Patient", "id": "123"}) @@ -23,7 +40,7 @@ def dummy_callback(request: httpx.Request) -> Response: if ( request.url.path == "/Patient" and str(request.url.params) - == "_has%3AResearchSubject%3Asubject%3Astudy=ResearchStudy%2F123&_revinclude=Group%3Amember&_count=1000&_total=accurate" + == "_has%3AResearchSubject%3Asubject%3Astudy=ResearchStudy%2F123&_revinclude=ResearchSubject%3Asubject&_revinclude=Group%3Amember&_count=1000&_total=accurate" ): return Response( 200, @@ -52,9 +69,129 @@ def dummy_callback(request: httpx.Request) -> Response: }, ) + if request.url.path == "/Group" and "member=Specimen" in str(request.url.params): + return Response( + 200, + json={ + "resourceType": "Bundle", + "type": "searchset", + "entry": [ + {"resource": {"resourceType": "Group", "id": "G1"}}, + ], + }, + ) + + if request.url.path == "/DocumentReference" and "subject=Group" in str(request.url.params): + return Response( + 200, + json={ + "resourceType": "Bundle", + "type": "searchset", + "entry": [ + {"resource": {"resourceType": "DocumentReference", "id": "DR1"}}, + ], + }, + ) + + if request.url.path == "/DocumentReference" and "subject=Patient" in str(request.url.params): + return Response( + 200, + json={ + "resourceType": "Bundle", + "type": "searchset", + "entry": [ + {"resource": {"resourceType": "DocumentReference", "id": "DR2"}}, + ], + }, + ) + + if request.url.path == "/Observation" and "subject=Patient" in str(request.url.params): + return Response( + 200, + json={ + "resourceType": "Bundle", + "type": "searchset", + "entry": [ + {"resource": {"resourceType": "Observation", "id": "O1"}}, + ], + }, + ) + + if request.url.path == "/Procedure" and "subject=Patient" in str(request.url.params): + return Response( + 200, + json={ + "resourceType": "Bundle", + "type": "searchset", + "entry": [ + {"resource": {"resourceType": "Procedure", "id": "P1"}}, + ], + }, + ) + + if request.url.path == "/ServiceRequest" and "subject=Patient" in str(request.url.params): + return Response( + 200, + json={ + "resourceType": "Bundle", + "type": "searchset", + "entry": [ + {"resource": {"resourceType": "ServiceRequest", "id": "SR1"}}, + ], + }, + ) + + if request.url.path == "/ImagingStudy" and "subject=Patient" in str(request.url.params): + return Response( + 200, + json={ + "resourceType": "Bundle", + "type": "searchset", + "entry": [ + {"resource": {"resourceType": "ImagingStudy", "id": "IS1"}}, + ], + }, + ) + + if request.url.path == "/Condition" and "subject=Patient" in str(request.url.params): + return Response( + 200, + json={ + "resourceType": "Bundle", + "type": "searchset", + "entry": [ + {"resource": {"resourceType": "Condition", "id": "C1"}}, + ], + }, + ) + + if request.url.path == "/Medication" and "subject=Patient" in str(request.url.params): + return Response( + 200, + json={ + "resourceType": "Bundle", + "type": "searchset", + "entry": [ + {"resource": {"resourceType": "Medication", "id": "M1"}}, + ], + }, + ) + + if request.url.path == "/MedicationAdministration" and "subject=Patient" in str(request.url.params): + return Response( + 200, + json={ + "resourceType": "Bundle", + "type": "searchset", + "entry": [ + {"resource": {"resourceType": "MedicationAdministration", "id": "MA1"}}, + ], + }, + ) + # unexpected request print(request.url, str(request.url.params)) - assert False, f"Unexpected url {request.url.path}, {str(request.url.params)}" + assert False, f"Unexpected url:{request.url} path:{request.url.path}, params:{str(request.url.params)}" httpx_mock.add_callback(dummy_callback) diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 95de108..e7a0930 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -22,8 +22,7 @@ def test_help_option() -> None: assert "--fhir-base-url" in output assert "--graph-definition-id" in output assert "--graph-definition-file-path" in output - assert "--start-resource-type" in output - assert "--start-resource-id" in output + assert "--path" in output assert "--db-path" in output assert "--dry-run" in output assert "--debug" in output diff --git a/tests/unit/test_mock_server.py b/tests/unit/test_mock_server.py index 7f95283..c817caa 100644 --- a/tests/unit/test_mock_server.py +++ b/tests/unit/test_mock_server.py @@ -34,14 +34,12 @@ def test_runner(tmp_path: str) -> None: [ "--fhir-base-url", "http://testserver", - "--start-resource-type", - "ResearchStudy", - "--start-resource-id", - "123", + "--path", + "/ResearchStudy?_id=123", "--db-path", f"{tmp_path}/fhir-query.sqlite", "--graph-definition-file-path", - "tests/fixtures/GraphDefinition.yaml", + "tests/fixtures/ResearchStudyGraph.yaml", "--log-file", f"{tmp_path}/fhir-query.log", "--debug", @@ -59,10 +57,41 @@ def test_runner(tmp_path: str) -> None: # test the database db = Dataframer(f"{tmp_path}/fhir-query.sqlite") - assert db.count_resource_types() == {"Patient": 3, "Specimen": 3} + count_resource_types = db.count_resource_types() + print(count_resource_types) + assert count_resource_types == { + "Condition": 1, + "DocumentReference": 2, + "Group": 1, + "ImagingStudy": 1, + "MedicationAdministration": 1, + "Observation": 1, + "Patient": 3, + "Procedure": 1, + "ResearchStudy": 1, + "ResearchSubject": 1, + "ServiceRequest": 1, + "Specimen": 3, + } aggregated = db.aggregate() - assert sorted(aggregated.keys()) == ["Patient", "Specimen"] + aggregated_keys = sorted(aggregated.keys()) + print(aggregated_keys) + assert aggregated_keys == [ + "Condition", + "DocumentReference", + "Group", + "ImagingStudy", + "MedicationAdministration", + "Observation", + "Patient", + "Procedure", + "ResearchStudy", + "ResearchSubject", + "ServiceRequest", + "Specimen", + ] + assert aggregated["Patient"]["count"] == 3 assert aggregated["Specimen"]["count"] == 3 assert aggregated["Specimen"]["references"]["Patient"]["count"] == 3