fhir-sheets 2.1.0__tar.gz → 2.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fhir-sheets might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fhir-sheets
3
- Version: 2.1.0
3
+ Version: 2.1.3
4
4
  Summary: FhirSheets is a command-line tool that reads an Excel file in FHIR cohort format and generates FHIR bundle JSON files from it. Each row in the template Excel file is used to create an individual JSON file, outputting them to a specified folder.
5
5
  License-File: LICENSE
6
6
  Author: Michael Riley
@@ -14,6 +14,7 @@ Requires-Dist: jsonpath-ng (==1.6.1)
14
14
  Requires-Dist: openpyxl (==3.1.5)
15
15
  Requires-Dist: orjson (==3.10.7)
16
16
  Requires-Dist: ply (==3.11)
17
+ Requires-Dist: pytest_cov (==7.0.0)
17
18
  Description-Content-Type: text/markdown
18
19
 
19
20
  # FHIRSheets
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "fhir-sheets"
3
- version = "2.1.0"
3
+ version = "2.1.3"
4
4
  description = "FhirSheets is a command-line tool that reads an Excel file in FHIR cohort format and generates FHIR bundle JSON files from it. Each row in the template Excel file is used to create an individual JSON file, outputting them to a specified folder."
5
5
  authors = ["Michael Riley <Michael.Riley@gtri.gatech.edu>"]
6
6
  packages = [{include = "fhir_sheets", from = "src"}]
@@ -14,6 +14,7 @@ jsonpath-ng = "1.6.1"
14
14
  openpyxl = "3.1.5"
15
15
  orjson = "3.10.7"
16
16
  ply = "3.11"
17
+ pytest_cov = "7.0.0"
17
18
 
18
19
  [build-system]
19
20
  requires = ["poetry-core"]
@@ -29,7 +29,6 @@ def main(input_file, output_folder):
29
29
  if not output_folder_path.exists():
30
30
  output_folder_path.mkdir(parents=True, exist_ok=True) # Create the folder if it doesn't exist
31
31
  resource_definition_entities, resource_link_entities, cohort_data = read_input.read_xlsx_and_process(input_file)
32
- pprint(cohort_data)
33
32
  #For each index of patients
34
33
  for i in range(0,cohort_data.get_num_patients()):
35
34
  # Construct the file path for each JSON file
@@ -27,6 +27,19 @@ def create_transaction_bundle(resource_definition_entities: List[ResourceDefinit
27
27
  add_resource_to_transaction_bundle(root_bundle, fhir_resource)
28
28
  return root_bundle
29
29
 
30
+ def create_singular_resource(singleton_entity_name: str, resource_definition_entities: List[ResourceDefinition], resource_link_entities: List[ResourceLink], cohort_data: CohortData, index = 0):
31
+ created_resources = {}
32
+ for resource_definition in resource_definition_entities:
33
+ entity_name = resource_definition.entity_name
34
+ #Create and collect fhir resources
35
+ fhir_resource = create_fhir_resource(resource_definition, cohort_data, index)
36
+ created_resources[entity_name] = fhir_resource
37
+ if entity_name == singleton_entity_name:
38
+ singleton_fhir_resource = fhir_resource
39
+ add_default_resource_links(created_resources, resource_link_entities)
40
+ create_resource_links(created_resources, resource_link_entities, preview_mode=True)
41
+ return singleton_fhir_resource
42
+
30
43
  #Initialize root bundle definition
31
44
  def initialize_bundle():
32
45
  root_bundle = {}
@@ -150,12 +163,21 @@ def add_default_resource_links(created_resources: dict, resource_link_entities:
150
163
  return
151
164
 
152
165
 
153
- #Create resource references/links with created entities
154
- def create_resource_links(created_resources, resource_link_entites):
166
+ #List function to create resource references/links with created entities
167
+ def create_resource_links(created_resources, resource_link_entites, preview_mode = False):
168
+ #TODO: Build resource links
169
+ print("Building resource links")
170
+ for resource_link_entity in resource_link_entites:
171
+ create_resource_link(created_resources, resource_link_entity, preview_mode)
172
+ return
173
+
174
+ #Singular function to create a resource link.
175
+ def create_resource_link(created_resources, resource_link_entity, preview_mode = False):
176
+ # template scaffolding
155
177
  reference_json_block = {
156
178
  "reference" : "$value"
157
179
  }
158
-
180
+ #Special reference handling blocks, in the form of (origin_resource, destination_resource, reference_path)
159
181
  arrayType_references = [
160
182
  ('diagnosticreport', 'specimen', 'specimen'),
161
183
  ('diagnosticreport', 'practitioner', 'performer'),
@@ -165,35 +187,36 @@ def create_resource_links(created_resources, resource_link_entites):
165
187
  ('diagnosticreport', 'observation', 'result'),
166
188
  ('diagnosticreport', 'imagingStudy', 'imagingStudy'),
167
189
  ]
168
- #TODO: Build resource links
169
- print("Building resource links")
170
- for resource_link_entity in resource_link_entites:
171
- try:
172
- origin_resource = created_resources[resource_link_entity.origin_resource]
173
- except KeyError:
174
- print(f"WARNING: In ResourceLinks tab, found a Origin Resource of : {resource_link_entity.origin_resource} but no such entity found in PatientData")
175
- continue
176
- try:
177
- destination_resource = created_resources[resource_link_entity.destination_resource]
178
- except KeyError:
179
- print(f"WARNING: In ResourceLinks tab, found a Desitnation Resource of : {resource_link_entity.destination_resource} but no such entity found in PatientData")
180
- continue
181
- destination_resource_type = created_resources[resource_link_entity.destination_resource]['resourceType']
182
- destination_resource_id = created_resources[resource_link_entity.destination_resource]['id']
183
- link_tuple = (created_resources[resource_link_entity.origin_resource]['resourceType'].strip().lower(),
184
- created_resources[resource_link_entity.destination_resource]['resourceType'].strip().lower(),
185
- resource_link_entity.reference_path.strip().lower())
186
- if link_tuple in arrayType_references:
187
- if resource_link_entity.reference_path.strip().lower() not in origin_resource:
188
- origin_resource[resource_link_entity.reference_path.strip().lower()] = []
189
- new_reference = reference_json_block.copy()
190
- new_reference['reference'] = destination_resource_type + "/" + destination_resource_id
191
- origin_resource[resource_link_entity.reference_path.strip().lower()].append(new_reference)
192
- else:
193
- origin_resource[resource_link_entity.reference_path.strip().lower()] = reference_json_block.copy()
194
- origin_resource[resource_link_entity.reference_path.strip().lower()]["reference"] = destination_resource_type + "/" + destination_resource_id
190
+ #Find the origin and destination resource from the link
191
+ try:
192
+ origin_resource = created_resources[resource_link_entity.origin_resource]
193
+ except KeyError:
194
+ print(f"WARNING: In ResourceLinks tab, found a Origin Resource of : {resource_link_entity.origin_resource} but no such entity found in PatientData")
195
+ return
196
+ try:
197
+ destination_resource = created_resources[resource_link_entity.destination_resource]
198
+ except KeyError:
199
+ print(f"WARNING: In ResourceLinks tab, found a Desitnation Resource of : {resource_link_entity.destination_resource} but no such entity found in PatientData")
200
+ return
201
+ #Estable the value of the refence
202
+ if preview_mode:
203
+ reference_value = destination_resource['resourceType'] + "/" + resource_link_entity.destination_resource
204
+ else:
205
+ reference_value = destination_resource['resourceType'] + "/" + destination_resource['id']
206
+ link_tuple = (origin_resource['resourceType'].strip().lower(),
207
+ destination_resource['resourceType'].strip().lower(),
208
+ resource_link_entity.reference_path.strip().lower())
209
+ if link_tuple in arrayType_references:
210
+ if resource_link_entity.reference_path.strip().lower() not in origin_resource:
211
+ origin_resource[resource_link_entity.reference_path.strip().lower()] = []
212
+ new_reference = reference_json_block.copy()
213
+ new_reference['reference'] = reference_value
214
+ origin_resource[resource_link_entity.reference_path.strip().lower()].append(new_reference)
215
+ else:
216
+ origin_resource[resource_link_entity.reference_path.strip().lower()] = reference_json_block.copy()
217
+ origin_resource[resource_link_entity.reference_path.strip().lower()]["reference"] = reference_value
195
218
  return
196
-
219
+
197
220
  def add_resource_to_transaction_bundle(root_bundle, fhir_resource):
198
221
  entry = {}
199
222
  entry['fullUrl'] = "urn:uuid:"+fhir_resource['id']
@@ -235,7 +258,7 @@ def build_structure(current_struct: Dict, json_path: str, resource_definition: R
235
258
  #SPECIAL HANDLING CLAUSE
236
259
  matching_handler = next((handler for handler in special_values.custom_handlers if (json_path.startswith(handler) or json_path == handler)), None)
237
260
  if matching_handler is not None:
238
- return special_values.custom_handlers[matching_handler].assign_value(json_path, resource_definition, current_struct, parts[-1], value)
261
+ return special_values.custom_handlers[matching_handler].assign_value(json_path, resource_definition, dataType, current_struct, parts[-1], value)
239
262
  #Ignore dollar sign ($) and drill farther down
240
263
  if part == '$' or part == resource_definition.resource_type.strip():
241
264
  #Ignore the dollar sign and the resourcetype
@@ -17,4 +17,4 @@ class ResourceDefinition:
17
17
  self.profiles = entity_data.get('Profile(s)')
18
18
 
19
19
  def __repr__(self) -> str:
20
- return f"FhirEntity(entity_name='{self.entity_name}', resource_type='{self.resource_type}', profiles={self.profiles})"
20
+ return f"ResourceDefinition(entity_name='{self.entity_name}', resource_type='{self.resource_type}', profiles={self.profiles})"
@@ -33,7 +33,7 @@ def process_sheet_resource_definitions(sheet):
33
33
  headers = [cell.value for cell in next(sheet.iter_rows(min_row=1, max_row=1))] # Get headers
34
34
 
35
35
  for row in sheet.iter_rows(min_row=3, values_only=True):
36
- row_data = dict(zip(headers, row)) # Create a dictionary for each row
36
+ row_data = dict((h, r) for h, r in zip(headers, row) if h is not None) # Create a dictionary for each row
37
37
  if all(cell is None or cell == "" for cell in row_data.values()):
38
38
  continue
39
39
  # Split 'Profile(s)' column into a list of URLs
@@ -41,7 +41,7 @@ def process_sheet_resource_definitions(sheet):
41
41
  row_data["Profile(s)"] = [url.strip() for url in row_data["Profile(s)"].split(",")]
42
42
  resource_definition_entities.append(ResourceDefinition(row_data))
43
43
  resource_definitions.append(row_data)
44
-
44
+ print(f"Resource Definitions\n----------{resource_definitions}")
45
45
  return resource_definition_entities
46
46
 
47
47
  # Function to process the specific sheet with 'OriginResource', 'ReferencePath', and 'DestinationResource'
@@ -55,57 +55,9 @@ def process_sheet_resource_links(sheet):
55
55
  continue
56
56
  resource_links.append(row_data)
57
57
  resource_link_entities.append(ResourceLink(row_data))
58
-
58
+ print(f"Resource Links\n----------{resource_links}")
59
59
  return resource_link_entities
60
60
 
61
- # Function to process the "PatientData" sheet
62
- def process_sheet_patient_data(sheet, resource_definition_entities):
63
- # Initialize the dictionary to store the processed data
64
- patient_data = {}
65
- cohort_data = CohortData()
66
- # Extract the data from the first 6 rows (Entity To Query, JsonPath, etc.)
67
- for col in sheet.iter_cols(min_row=1, max_row=6, min_col=3, values_only=True): # Start from 3rd column
68
- if all(entry is None for entry in col):
69
- continue
70
- entity_name = col[0] # The entity name comes from the first row (Entity To Query)
71
- field_name = col[5] #The "Data Element" comes from the fifth row
72
- if (entity_name is None or entity_name == "") and (field_name is not None and field_name != ""):
73
- print(f"WARNING: - Reading Patient Data Issue - {field_name} - 'Entity To Query' cell missing for column labelled '{field_name}', please provide entity name from the ResourceDefinitions tab.")
74
-
75
- if entity_name not in [entry.entity_name for entry in resource_definition_entities]:
76
- print(f"WARNING: - Reading Patient Data Issue - {field_name} - 'Entity To Query' cell has entity named '{entity_name}', however, the ResourceDefinition tab has no matching resource. Please provide a corresponding entry in the ResourceDefinition tab.")
77
- # Create structure for this entity if not already present
78
- if entity_name not in patient_data:
79
- patient_data[entity_name] = {}
80
- cohort_data.insert_entity(entity_name, EntityData({}))
81
-
82
- # Add jsonpath, valuesets, and initialize an empty list for 'values'
83
- if field_name not in patient_data[entity_name]:
84
- field_data = {
85
- "jsonpath": col[1], # JsonPath from the second row
86
- "valueType": col[2], # Value Type from the third row
87
- "valuesets": col[3], # Value Set from the fourth row
88
- "values": [] # Initialize empty list for actual values
89
- }
90
- patient_data[entity_name][field_name] = field_data
91
- cohort_data.entities[entity_name].insert(field_name, FieldEntry(field_data))
92
-
93
- # Now process the rows starting from the 6th row (the actual data entries)
94
- num_entries = 0
95
- for row in sheet.iter_rows(min_row=7, values_only=True): # Start from row 6 for actual data
96
- if all(cell is None for cell in row):
97
- continue
98
- num_entries = num_entries + 1
99
- entity_name = row[0] # The entity name comes from the first column of each row
100
- for i, value in enumerate(row[2:], start=1): # Iterate through the values in the columns
101
- entity_name = sheet.cell(row=1, column=i + 2).value
102
- field_name = sheet.cell(row=6, column=i + 2).value # Get the Data Element for this column
103
- if entity_name in patient_data and field_name in patient_data[entity_name]:
104
- # Append the actual data values to the 'values' array
105
- cohort_data.entities[entity_name].fields[field_name].values.append(value)
106
- cohort_data.num_entries = num_entries
107
- return cohort_data
108
-
109
61
  # Function to process the "PatientData" sheet for the Revised CohortData
110
62
  def process_sheet_patient_data_revised(sheet, resource_definition_entities):
111
63
  headers = []
@@ -141,5 +93,7 @@ def process_sheet_patient_data_revised(sheet, resource_definition_entities):
141
93
  patients.extend([{}] * needed_count)
142
94
  for patient_dict, value in zip(patients, values):
143
95
  patient_dict[(entity_name, field_name)] = value
96
+ print(f"Headers\n----------{headers}")
97
+ print(f"Patients\n----------{patients}")
144
98
  cohort_data = CohortData(headers=headers, patients=patients)
145
99
  return cohort_data
@@ -7,7 +7,7 @@ from abc import ABC, abstractmethod
7
7
  class AbstractCustomValueHandler(ABC):
8
8
 
9
9
  @abstractmethod
10
- def assign_value(self, json_path, resource_definition, final_struct, key, value):
10
+ def assign_value(self, json_path, resource_definition, dataType, final_struct, key, value):
11
11
  pass
12
12
 
13
13
  class PatientRaceExtensionValueHandler(AbstractCustomValueHandler):
@@ -67,7 +67,7 @@ class PatientRaceExtensionValueHandler(AbstractCustomValueHandler):
67
67
  "url" : "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race"
68
68
  }
69
69
  #Create an ombcategory and detailed section of race extension
70
- def assign_value(self, json_path, resource_definition, final_struct, key, value):
70
+ def assign_value(self, json_path, resource_definition, dataType, final_struct, key, value):
71
71
  #Retrieve the race extension if it exists; make it if it does not.
72
72
  if 'extension' not in final_struct:
73
73
  final_struct['extension'] = []
@@ -128,7 +128,7 @@ class PatientEthnicityExtensionValueHandler(AbstractCustomValueHandler):
128
128
  "url" : "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity"
129
129
  }
130
130
  #Create an ombcategory and detailed section of ethnicitiy extension
131
- def assign_value(self, json_path, resource_definition, final_struct, key, value):
131
+ def assign_value(self, json_path, resource_definition, dataType, final_struct, key, value):
132
132
  #Retrieve the ethncitiy extension if it exists; make it if it does not.
133
133
  if 'extension' not in final_struct:
134
134
  final_struct['extension'] = []
@@ -154,7 +154,7 @@ class PatientBirthSexExtensionValueHandler(AbstractCustomValueHandler):
154
154
  "valueCode" : "$value"
155
155
  }
156
156
  #Assigna birthsex extension
157
- def assign_value(self, json_path, resource_definition, final_struct, key, value):
157
+ def assign_value(self, json_path, resource_definition, dataType, final_struct, key, value):
158
158
  #Retrieve the birthsex extension if it exists; make it if it does not.
159
159
  if 'extension' not in final_struct:
160
160
  final_struct['extension'] = []
@@ -182,7 +182,7 @@ class PatientMRNIdentifierValueHandler(AbstractCustomValueHandler):
182
182
  "value" : "$value"
183
183
  }
184
184
  #Assign a MRN identifier
185
- def assign_value(self, json_path, resource_definition, final_struct, key, value):
185
+ def assign_value(self, json_path, resource_definition, dataType, final_struct, key, value):
186
186
  #Retrieve the MRN identifier if it exists; make it if it does not.
187
187
  target_identifier = self.patient_mrn_block
188
188
  new_identifier = True
@@ -215,7 +215,7 @@ class PatientSSNIdentifierValueHandler(AbstractCustomValueHandler):
215
215
  "value" : "$value"
216
216
  }
217
217
  #Assign a MRN identifier
218
- def assign_value(self, json_path, resource_definition, final_struct, key, value):
218
+ def assign_value(self, json_path, resource_definition, dataType, final_struct, key, value):
219
219
  #Retrieve the MRN identifier if it exists; make it if it does not.
220
220
  target_identifier = self.patient_mrn_block
221
221
  new_identifier = True
@@ -238,7 +238,7 @@ class OrganizationIdentiferNPIValueHandler(AbstractCustomValueHandler):
238
238
  "value" : "$value"
239
239
  }
240
240
  #Assigna birthsex extension
241
- def assign_value(self, json_path, resource_definition, final_struct, key, value):
241
+ def assign_value(self, json_path, resource_definition, dataType, final_struct, key, value):
242
242
  #Retrieve the birthsex extension if it exists; make it if it does not.
243
243
  if 'identifier' not in final_struct:
244
244
  final_struct['identifier'] = []
@@ -255,7 +255,7 @@ class OrganizationIdentiferCLIAValueHandler(AbstractCustomValueHandler):
255
255
  "value" : "$value"
256
256
  }
257
257
  #Assign a birthsex extension
258
- def assign_value(self, json_path, resource_definition, final_struct, key, value):
258
+ def assign_value(self, json_path, resource_definition, dataType, final_struct, key, value):
259
259
  #Retrieve the birthsex extension if it exists; make it if it does not.
260
260
  if 'identifier' not in final_struct:
261
261
  final_struct['identifier'] = []
@@ -272,7 +272,7 @@ class PractitionerIdentiferNPIValueHandler(AbstractCustomValueHandler):
272
272
  "value" : "$value"
273
273
  }
274
274
  #Assigna birthsex extension
275
- def assign_value(self, json_path, resource_definition, final_struct, key, value):
275
+ def assign_value(self, json_path, resource_definition, dataType, final_struct, key, value):
276
276
  #Retrieve the birthsex extension if it exists; make it if it does not.
277
277
  if 'identifier' not in final_struct:
278
278
  final_struct['identifier'] = []
@@ -309,7 +309,7 @@ class ObservationComponentHandler(AbstractCustomValueHandler):
309
309
  }
310
310
  }
311
311
  #Find the appropriate component for the observaiton; then call build_structure again to continue the drill down
312
- def assign_value(self, json_path, resource_definition, final_struct, key, value):
312
+ def assign_value(self, json_path, resource_definition, dataType, final_struct, key, value):
313
313
  #Check to make sure the component part exists
314
314
  if 'component' not in final_struct:
315
315
  final_struct['component'] = []
@@ -330,7 +330,8 @@ class ObservationComponentHandler(AbstractCustomValueHandler):
330
330
  if target_component is self.pulse_oximetry_oxygen_concentration:
331
331
  components.append(target_component)
332
332
  #Recurse back down into
333
- return conversion.build_structure(target_component, '.'.join(parts[2:]), resource_definition, parts[2:], value, parts[:2])
333
+ # current_struct: Dict, json_path: str, resource_definition: ResourceDefinition, dataType: str, parts: List[str], value: Any, previous_parts: List[str]
334
+ return conversion.build_structure(target_component, '.'.join(parts[2:]), resource_definition, dataType, parts[2:], value, parts[:2])
334
335
  pass
335
336
 
336
337
  def utilFindExtensionWithURL(extension_block, url):
File without changes
File without changes