Ali2206 commited on
Commit
f08b86a
·
1 Parent(s): 8c49beb

device token

Browse files
api/routes/patients.py CHANGED
@@ -6,7 +6,8 @@ from utils.helpers import calculate_age, standardize_language
6
  from models.entities import Note, PatientCreate
7
  from models.schemas import PatientListResponse # Fixed import
8
  from api.services.fhir_integration import HAPIFHIRIntegrationService
9
- from datetime import datetime
 
10
  from bson import ObjectId
11
  from bson.errors import InvalidId
12
  from typing import Optional, List, Dict, Any
@@ -755,6 +756,131 @@ async def get_patients(
755
  detail=f"Error fetching patients: {str(e)}"
756
  )
757
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
758
  @router.get("/patients/{patient_id}", response_model=dict)
759
  async def get_patient(patient_id: str):
760
  logger.info(f"Retrieving patient: {patient_id}")
@@ -1192,130 +1318,7 @@ async def get_hapi_statistics(
1192
  detail=f"Failed to get HAPI FHIR statistics: {str(e)}"
1193
  )
1194
 
1195
- @router.get("/patients/ehr-systems")
1196
- async def get_ehr_systems(current_user: dict = Depends(get_current_user)):
1197
- """
1198
- Get available EHR systems and their configurations
1199
- """
1200
- ehr_systems = [
1201
- {
1202
- "id": "epic",
1203
- "name": "Epic",
1204
- "description": "Epic EHR System",
1205
- "base_url": "https://fhir.epic.com/api/FHIR/R4",
1206
- "auth_type": "oauth2",
1207
- "supported_resources": ["Patient", "Condition", "Medication", "Observation"],
1208
- "field_mapping": {
1209
- "full_name": "name[0].text",
1210
- "date_of_birth": "birthDate",
1211
- "gender": "gender",
1212
- "address": "address[0].text",
1213
- "national_id": "identifier[?type.coding[0].code=='SS'].value",
1214
- "blood_type": "extension[?url=='http://hl7.org/fhir/StructureDefinition/patient-bloodType'].valueCodeableConcept.text",
1215
- "allergies": "allergyIntolerance[].substance.text",
1216
- "chronic_conditions": "condition[?category.coding[0].code=='problem-list-item'].code.text",
1217
- "medications": "medicationRequest[].medicationCodeableConcept.text",
1218
- "emergency_contact_name": "contact[?relationship[0].coding[0].code=='emergency'].name.text",
1219
- "emergency_contact_phone": "contact[?relationship[0].coding[0].code=='emergency'].telecom[?system=='phone'].value",
1220
- "insurance_provider": "coverage[].payor[0].display",
1221
- "insurance_policy_number": "coverage[].subscriberId"
1222
- }
1223
- },
1224
- {
1225
- "id": "cerner",
1226
- "name": "Cerner",
1227
- "description": "Cerner EHR System",
1228
- "base_url": "https://fhir.cerner.com/millennium/r4",
1229
- "auth_type": "oauth2",
1230
- "supported_resources": ["Patient", "Condition", "Medication", "Observation"],
1231
- "field_mapping": {
1232
- "full_name": "name[0].text",
1233
- "date_of_birth": "birthDate",
1234
- "gender": "gender",
1235
- "address": "address[0].text",
1236
- "national_id": "identifier[?type.coding[0].code=='SS'].value",
1237
- "blood_type": "extension[?url=='http://cerner.com/fhir/StructureDefinition/patient-bloodType'].valueCodeableConcept.text",
1238
- "allergies": "allergyIntolerance[].substance.text",
1239
- "chronic_conditions": "condition[?category.coding[0].code=='problem-list-item'].code.text",
1240
- "medications": "medicationRequest[].medicationCodeableConcept.text",
1241
- "emergency_contact_name": "contact[?relationship[0].coding[0].code=='emergency'].name.text",
1242
- "emergency_contact_phone": "contact[?relationship[0].coding[0].code=='emergency'].telecom[?system=='phone'].value",
1243
- "insurance_provider": "coverage[].payor[0].display",
1244
- "insurance_policy_number": "coverage[].subscriberId"
1245
- }
1246
- },
1247
- {
1248
- "id": "allscripts",
1249
- "name": "Allscripts",
1250
- "description": "Allscripts EHR System",
1251
- "base_url": "https://fhir.allscripts.com/r4",
1252
- "auth_type": "oauth2",
1253
- "supported_resources": ["Patient", "Condition", "Medication", "Observation"],
1254
- "field_mapping": {
1255
- "full_name": "name[0].text",
1256
- "date_of_birth": "birthDate",
1257
- "gender": "gender",
1258
- "address": "address[0].text",
1259
- "national_id": "identifier[?type.coding[0].code=='SS'].value",
1260
- "blood_type": "extension[?url=='http://allscripts.com/fhir/StructureDefinition/patient-bloodType'].valueCodeableConcept.text",
1261
- "allergies": "allergyIntolerance[].substance.text",
1262
- "chronic_conditions": "condition[?category.coding[0].code=='problem-list-item'].code.text",
1263
- "medications": "medicationRequest[].medicationCodeableConcept.text",
1264
- "emergency_contact_name": "contact[?relationship[0].coding[0].code=='emergency'].name.text",
1265
- "emergency_contact_phone": "contact[?relationship[0].coding[0].code=='emergency'].telecom[?system=='phone'].value",
1266
- "insurance_provider": "coverage[].payor[0].display",
1267
- "insurance_policy_number": "coverage[].subscriberId"
1268
- }
1269
- },
1270
- {
1271
- "id": "meditech",
1272
- "name": "Meditech",
1273
- "description": "Meditech EHR System",
1274
- "base_url": "https://fhir.meditech.com/r4",
1275
- "auth_type": "oauth2",
1276
- "supported_resources": ["Patient", "Condition", "Medication", "Observation"],
1277
- "field_mapping": {
1278
- "full_name": "name[0].text",
1279
- "date_of_birth": "birthDate",
1280
- "gender": "gender",
1281
- "address": "address[0].text",
1282
- "national_id": "identifier[?type.coding[0].code=='SS'].value",
1283
- "blood_type": "extension[?url=='http://meditech.com/fhir/StructureDefinition/patient-bloodType'].valueCodeableConcept.text",
1284
- "allergies": "allergyIntolerance[].substance.text",
1285
- "chronic_conditions": "condition[?category.coding[0].code=='problem-list-item'].code.text",
1286
- "medications": "medicationRequest[].medicationCodeableConcept.text",
1287
- "emergency_contact_name": "contact[?relationship[0].coding[0].code=='emergency'].name.text",
1288
- "emergency_contact_phone": "contact[?relationship[0].coding[0].code=='emergency'].telecom[?system=='phone'].value",
1289
- "insurance_provider": "coverage[].payor[0].display",
1290
- "insurance_policy_number": "coverage[].subscriberId"
1291
- }
1292
- },
1293
- {
1294
- "id": "hapi_fhir_test",
1295
- "name": "HAPI FHIR Test Server",
1296
- "description": "Public HAPI FHIR Test Server for development and testing",
1297
- "base_url": "https://hapi.fhir.org/baseR4",
1298
- "auth_type": "none",
1299
- "supported_resources": ["Patient", "Condition", "Medication", "Observation", "AllergyIntolerance"],
1300
- "field_mapping": {
1301
- "full_name": "name[0].text",
1302
- "date_of_birth": "birthDate",
1303
- "gender": "gender",
1304
- "address": "address[0].text",
1305
- "national_id": "identifier[?type.coding[0].code=='SS'].value",
1306
- "blood_type": "extension[?url=='http://hl7.org/fhir/StructureDefinition/patient-bloodType'].valueCodeableConcept.text",
1307
- "allergies": "allergyIntolerance[].substance.text",
1308
- "chronic_conditions": "condition[?category.coding[0].code=='problem-list-item'].code.text",
1309
- "medications": "medicationRequest[].medicationCodeableConcept.text",
1310
- "emergency_contact_name": "contact[?relationship[0].coding[0].code=='emergency'].name.text",
1311
- "emergency_contact_phone": "contact[?relationship[0].coding[0].code=='emergency'].telecom[?system=='phone'].value",
1312
- "insurance_provider": "coverage[].payor[0].display",
1313
- "insurance_policy_number": "coverage[].subscriberId"
1314
- }
1315
- }
1316
- ]
1317
-
1318
- return {"ehr_systems": ehr_systems}
1319
 
1320
  @router.post("/patients/fetch-ehr-data")
1321
  async def fetch_ehr_data(
@@ -1486,5 +1489,239 @@ def generate_sample_ehr_data(ehr_system: str, limit: int) -> List[dict]:
1486
 
1487
  return data
1488
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1489
  # Export the router as 'patients' for api.__init__.py
1490
  patients = router
 
6
  from models.entities import Note, PatientCreate
7
  from models.schemas import PatientListResponse # Fixed import
8
  from api.services.fhir_integration import HAPIFHIRIntegrationService
9
+ from api.services.synthea_integration import SyntheaIntegrationService
10
+ from datetime import datetime, timedelta
11
  from bson import ObjectId
12
  from bson.errors import InvalidId
13
  from typing import Optional, List, Dict, Any
 
756
  detail=f"Error fetching patients: {str(e)}"
757
  )
758
 
759
+ @router.get("/patients/ehr-systems")
760
+ async def get_ehr_systems(current_user: dict = Depends(get_current_user)):
761
+ """
762
+ Get available EHR systems and their configurations
763
+ """
764
+ ehr_systems = [
765
+ {
766
+ "id": "epic",
767
+ "name": "Epic",
768
+ "description": "Epic EHR System",
769
+ "base_url": "https://fhir.epic.com/api/FHIR/R4",
770
+ "auth_type": "oauth2",
771
+ "supported_resources": ["Patient", "Condition", "Medication", "Observation"],
772
+ "field_mapping": {
773
+ "full_name": "name[0].text",
774
+ "date_of_birth": "birthDate",
775
+ "gender": "gender",
776
+ "address": "address[0].text",
777
+ "national_id": "identifier[?type.coding[0].code=='SS'].value",
778
+ "blood_type": "extension[?url=='http://hl7.org/fhir/StructureDefinition/patient-bloodType'].valueCodeableConcept.text",
779
+ "allergies": "allergyIntolerance[].substance.text",
780
+ "chronic_conditions": "condition[?category.coding[0].code=='problem-list-item'].code.text",
781
+ "medications": "medicationRequest[].medicationCodeableConcept.text",
782
+ "emergency_contact_name": "contact[?relationship[0].coding[0].code=='emergency'].name.text",
783
+ "emergency_contact_phone": "contact[?relationship[0].coding[0].code=='emergency'].telecom[?system=='phone'].value",
784
+ "insurance_provider": "coverage[].payor[0].display",
785
+ "insurance_policy_number": "coverage[].subscriberId"
786
+ }
787
+ },
788
+ {
789
+ "id": "cerner",
790
+ "name": "Cerner",
791
+ "description": "Cerner EHR System",
792
+ "base_url": "https://fhir.cerner.com/millennium/r4",
793
+ "auth_type": "oauth2",
794
+ "supported_resources": ["Patient", "Condition", "Medication", "Observation"],
795
+ "field_mapping": {
796
+ "full_name": "name[0].text",
797
+ "date_of_birth": "birthDate",
798
+ "gender": "gender",
799
+ "address": "address[0].text",
800
+ "national_id": "identifier[?type.coding[0].code=='SS'].value",
801
+ "blood_type": "extension[?url=='http://cerner.com/fhir/StructureDefinition/patient-bloodType'].valueCodeableConcept.text",
802
+ "allergies": "allergyIntolerance[].substance.text",
803
+ "chronic_conditions": "condition[?category.coding[0].code=='problem-list-item'].code.text",
804
+ "medications": "medicationRequest[].medicationCodeableConcept.text",
805
+ "emergency_contact_name": "contact[?relationship[0].coding[0].code=='emergency'].name.text",
806
+ "emergency_contact_phone": "contact[?relationship[0].coding[0].code=='emergency'].telecom[?system=='phone'].value",
807
+ "insurance_provider": "coverage[].payor[0].display",
808
+ "insurance_policy_number": "coverage[].subscriberId"
809
+ }
810
+ },
811
+ {
812
+ "id": "allscripts",
813
+ "name": "Allscripts",
814
+ "description": "Allscripts EHR System",
815
+ "base_url": "https://fhir.allscripts.com/r4",
816
+ "auth_type": "oauth2",
817
+ "supported_resources": ["Patient", "Condition", "Medication", "Observation"],
818
+ "field_mapping": {
819
+ "full_name": "name[0].text",
820
+ "date_of_birth": "birthDate",
821
+ "gender": "gender",
822
+ "address": "address[0].text",
823
+ "national_id": "identifier[?type.coding[0].code=='SS'].value",
824
+ "blood_type": "extension[?url=='http://allscripts.com/fhir/StructureDefinition/patient-bloodType'].valueCodeableConcept.text",
825
+ "allergies": "allergyIntolerance[].substance.text",
826
+ "chronic_conditions": "condition[?category.coding[0].code=='problem-list-item'].code.text",
827
+ "medications": "medicationRequest[].medicationCodeableConcept.text",
828
+ "emergency_contact_name": "contact[?relationship[0].coding[0].code=='emergency'].name.text",
829
+ "emergency_contact_phone": "contact[?relationship[0].coding[0].code=='emergency'].telecom[?system=='phone'].value",
830
+ "insurance_provider": "coverage[].payor[0].display",
831
+ "insurance_policy_number": "coverage[].subscriberId"
832
+ }
833
+ },
834
+ {
835
+ "id": "meditech",
836
+ "name": "Meditech",
837
+ "description": "Meditech EHR System",
838
+ "base_url": "https://fhir.meditech.com/r4",
839
+ "auth_type": "oauth2",
840
+ "supported_resources": ["Patient", "Condition", "Medication", "Observation"],
841
+ "field_mapping": {
842
+ "full_name": "name[0].text",
843
+ "date_of_birth": "birthDate",
844
+ "gender": "gender",
845
+ "address": "address[0].text",
846
+ "national_id": "identifier[?type.coding[0].code=='SS'].value",
847
+ "blood_type": "extension[?url=='http://meditech.com/fhir/StructureDefinition/patient-bloodType'].valueCodeableConcept.text",
848
+ "allergies": "allergyIntolerance[].substance.text",
849
+ "chronic_conditions": "condition[?category.coding[0].code=='problem-list-item'].code.text",
850
+ "medications": "medicationRequest[].medicationCodeableConcept.text",
851
+ "emergency_contact_name": "contact[?relationship[0].coding[0].code=='emergency'].name.text",
852
+ "emergency_contact_phone": "contact[?relationship[0].coding[0].code=='emergency'].telecom[?system=='phone'].value",
853
+ "insurance_provider": "coverage[].payor[0].display",
854
+ "insurance_policy_number": "coverage[].subscriberId"
855
+ }
856
+ },
857
+ {
858
+ "id": "hapi_fhir_test",
859
+ "name": "HAPI FHIR Test Server",
860
+ "description": "Public HAPI FHIR Test Server for development and testing",
861
+ "base_url": "https://hapi.fhir.org/baseR4",
862
+ "auth_type": "none",
863
+ "supported_resources": ["Patient", "Condition", "Medication", "Observation", "AllergyIntolerance"],
864
+ "field_mapping": {
865
+ "full_name": "name[0].text",
866
+ "date_of_birth": "birthDate",
867
+ "gender": "gender",
868
+ "address": "address[0].text",
869
+ "national_id": "identifier[?type.coding[0].code=='SS'].value",
870
+ "blood_type": "extension[?url=='http://hl7.org/fhir/StructureDefinition/patient-bloodType'].valueCodeableConcept.text",
871
+ "allergies": "allergyIntolerance[].substance.text",
872
+ "chronic_conditions": "condition[?category.coding[0].code=='problem-list-item'].code.text",
873
+ "medications": "medicationRequest[].medicationCodeableConcept.text",
874
+ "emergency_contact_name": "contact[?relationship[0].coding[0].code=='emergency'].name.text",
875
+ "emergency_contact_phone": "contact[?relationship[0].coding[0].code=='emergency'].telecom[?system=='phone'].value",
876
+ "insurance_provider": "coverage[].payor[0].display",
877
+ "insurance_policy_number": "coverage[].subscriberId"
878
+ }
879
+ }
880
+ ]
881
+
882
+ return {"ehr_systems": ehr_systems}
883
+
884
  @router.get("/patients/{patient_id}", response_model=dict)
885
  async def get_patient(patient_id: str):
886
  logger.info(f"Retrieving patient: {patient_id}")
 
1318
  detail=f"Failed to get HAPI FHIR statistics: {str(e)}"
1319
  )
1320
 
1321
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1322
 
1323
  @router.post("/patients/fetch-ehr-data")
1324
  async def fetch_ehr_data(
 
1489
 
1490
  return data
1491
 
1492
+ @router.post("/patients/generate-synthea", status_code=status.HTTP_201_CREATED)
1493
+ async def generate_synthea_patients(
1494
+ population: int = Query(10, ge=1, le=100, description="Number of patients to generate"),
1495
+ age_min: int = Query(18, ge=0, le=120, description="Minimum age for generated patients"),
1496
+ age_max: int = Query(80, ge=0, le=120, description="Maximum age for generated patients"),
1497
+ gender: str = Query("both", description="Gender distribution: male, female, or both"),
1498
+ location: str = Query("Massachusetts", description="Location for generated patients"),
1499
+ current_user: dict = Depends(get_current_user)
1500
+ ):
1501
+ """
1502
+ Generate synthetic patient data using Synthea FHIR
1503
+ """
1504
+ logger.info(f"🎯 Generating {population} Synthea patients by user {current_user.get('email')}")
1505
+
1506
+ if not any(role in current_user.get('roles', []) for role in ['admin', 'doctor']):
1507
+ logger.warning(f"Unauthorized Synthea generation attempt by {current_user.get('email')}")
1508
+ raise HTTPException(
1509
+ status_code=status.HTTP_403_FORBIDDEN,
1510
+ detail="Only administrators and doctors can generate synthetic data"
1511
+ )
1512
+
1513
+ try:
1514
+ service = SyntheaIntegrationService()
1515
+ result = await service.generate_and_import_patients(
1516
+ population=population,
1517
+ age_min=age_min,
1518
+ age_max=age_max,
1519
+ gender=gender,
1520
+ location=location
1521
+ )
1522
+
1523
+ return {
1524
+ "message": f"Successfully generated {result['generated_patients']} synthetic patients",
1525
+ "generated_count": result['generated_patients'],
1526
+ "config": result['config'],
1527
+ "output_directory": result['output_directory'],
1528
+ "patients_ready_for_import": len(result['patients'])
1529
+ }
1530
+
1531
+ except Exception as e:
1532
+ logger.error(f"Error generating Synthea patients: {str(e)}")
1533
+ raise HTTPException(
1534
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1535
+ detail=f"Failed to generate Synthea patients: {str(e)}"
1536
+ )
1537
+
1538
+ @router.post("/patients/import-synthea", status_code=status.HTTP_201_CREATED)
1539
+ async def import_synthea_patients(
1540
+ current_user: dict = Depends(get_current_user)
1541
+ ):
1542
+ """
1543
+ Import previously generated Synthea patients into the database
1544
+ """
1545
+ logger.info(f"📥 Importing Synthea patients by user {current_user.get('email')}")
1546
+
1547
+ if not any(role in current_user.get('roles', []) for role in ['admin', 'doctor']):
1548
+ logger.warning(f"Unauthorized Synthea import attempt by {current_user.get('email')}")
1549
+ raise HTTPException(
1550
+ status_code=status.HTTP_403_FORBIDDEN,
1551
+ detail="Only administrators and doctors can import synthetic data"
1552
+ )
1553
+
1554
+ try:
1555
+ service = SyntheaIntegrationService()
1556
+ patients = await service.process_synthea_output()
1557
+
1558
+ if not patients:
1559
+ return {
1560
+ "message": "No Synthea patients found to import",
1561
+ "imported_count": 0,
1562
+ "skipped_count": 0,
1563
+ "errors": []
1564
+ }
1565
+
1566
+ imported_count = 0
1567
+ skipped_count = 0
1568
+ errors = []
1569
+
1570
+ for patient_data in patients:
1571
+ try:
1572
+ # Check for existing patient by FHIR ID
1573
+ existing_patient = await patients_collection.find_one({
1574
+ "fhir_id": patient_data['fhir_id']
1575
+ })
1576
+
1577
+ if existing_patient:
1578
+ skipped_count += 1
1579
+ continue
1580
+
1581
+ # Insert patient
1582
+ result = await patients_collection.insert_one(patient_data)
1583
+ imported_count += 1
1584
+
1585
+ logger.info(f"Imported Synthea patient {patient_data['full_name']} with FHIR ID {patient_data['fhir_id']}")
1586
+
1587
+ except Exception as e:
1588
+ error_msg = f"Error importing patient {patient_data.get('full_name', 'Unknown')}: {str(e)}"
1589
+ errors.append(error_msg)
1590
+ logger.error(error_msg)
1591
+
1592
+ return {
1593
+ "message": f"Import completed: {imported_count} imported, {skipped_count} skipped, {len(errors)} errors",
1594
+ "imported_count": imported_count,
1595
+ "skipped_count": skipped_count,
1596
+ "error_count": len(errors),
1597
+ "errors": errors,
1598
+ "source": "synthea"
1599
+ }
1600
+
1601
+ except Exception as e:
1602
+ logger.error(f"Error importing Synthea patients: {str(e)}")
1603
+ raise HTTPException(
1604
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1605
+ detail=f"Failed to import Synthea patients: {str(e)}"
1606
+ )
1607
+
1608
+ @router.get("/patients/synthea/statistics")
1609
+ async def get_synthea_statistics(
1610
+ current_user: dict = Depends(get_current_user)
1611
+ ):
1612
+ """
1613
+ Get statistics about Synthea-generated data
1614
+ """
1615
+ try:
1616
+ service = SyntheaIntegrationService()
1617
+ stats = await service.get_synthea_statistics()
1618
+
1619
+ # Get database statistics for Synthea patients
1620
+ db_stats = {
1621
+ "total_synthea_patients": await patients_collection.count_documents({"source": "synthea"}),
1622
+ "recent_synthea_patients": await patients_collection.count_documents({
1623
+ "source": "synthea",
1624
+ "import_date": {"$gte": (datetime.utcnow() - timedelta(days=7)).isoformat()}
1625
+ })
1626
+ }
1627
+
1628
+ return {
1629
+ "file_statistics": stats,
1630
+ "database_statistics": db_stats
1631
+ }
1632
+
1633
+ except Exception as e:
1634
+ logger.error(f"Error getting Synthea statistics: {str(e)}")
1635
+ raise HTTPException(
1636
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1637
+ detail=f"Failed to get Synthea statistics: {str(e)}"
1638
+ )
1639
+
1640
+ @router.post("/patients/generate-and-import-synthea", status_code=status.HTTP_201_CREATED)
1641
+ async def generate_and_import_synthea_patients(
1642
+ population: int = Query(10, ge=1, le=100, description="Number of patients to generate"),
1643
+ age_min: int = Query(18, ge=0, le=120, description="Minimum age for generated patients"),
1644
+ age_max: int = Query(80, ge=0, le=120, description="Maximum age for generated patients"),
1645
+ gender: str = Query("both", description="Gender distribution: male, female, or both"),
1646
+ location: str = Query("Massachusetts", description="Location for generated patients"),
1647
+ current_user: dict = Depends(get_current_user)
1648
+ ):
1649
+ """
1650
+ Generate and immediately import synthetic patient data using Synthea FHIR
1651
+ """
1652
+ logger.info(f"🎯 Generating and importing {population} Synthea patients by user {current_user.get('email')}")
1653
+
1654
+ if not any(role in current_user.get('roles', []) for role in ['admin', 'doctor']):
1655
+ logger.warning(f"Unauthorized Synthea generation/import attempt by {current_user.get('email')}")
1656
+ raise HTTPException(
1657
+ status_code=status.HTTP_403_FORBIDDEN,
1658
+ detail="Only administrators and doctors can generate and import synthetic data"
1659
+ )
1660
+
1661
+ try:
1662
+ # Step 1: Generate Synthea data
1663
+ service = SyntheaIntegrationService()
1664
+ generation_result = await service.generate_and_import_patients(
1665
+ population=population,
1666
+ age_min=age_min,
1667
+ age_max=age_max,
1668
+ gender=gender,
1669
+ location=location
1670
+ )
1671
+
1672
+ if not generation_result['patients']:
1673
+ return {
1674
+ "message": "No patients were generated",
1675
+ "generated_count": 0,
1676
+ "imported_count": 0,
1677
+ "skipped_count": 0,
1678
+ "errors": []
1679
+ }
1680
+
1681
+ # Step 2: Import generated patients
1682
+ imported_count = 0
1683
+ skipped_count = 0
1684
+ errors = []
1685
+
1686
+ for patient_data in generation_result['patients']:
1687
+ try:
1688
+ # Check for existing patient by FHIR ID
1689
+ existing_patient = await patients_collection.find_one({
1690
+ "fhir_id": patient_data['fhir_id']
1691
+ })
1692
+
1693
+ if existing_patient:
1694
+ skipped_count += 1
1695
+ continue
1696
+
1697
+ # Insert patient
1698
+ result = await patients_collection.insert_one(patient_data)
1699
+ imported_count += 1
1700
+
1701
+ logger.info(f"Imported Synthea patient {patient_data['full_name']} with FHIR ID {patient_data['fhir_id']}")
1702
+
1703
+ except Exception as e:
1704
+ error_msg = f"Error importing patient {patient_data.get('full_name', 'Unknown')}: {str(e)}"
1705
+ errors.append(error_msg)
1706
+ logger.error(error_msg)
1707
+
1708
+ return {
1709
+ "message": f"Generation and import completed: {generation_result['generated_patients']} generated, {imported_count} imported, {skipped_count} skipped, {len(errors)} errors",
1710
+ "generated_count": generation_result['generated_patients'],
1711
+ "imported_count": imported_count,
1712
+ "skipped_count": skipped_count,
1713
+ "error_count": len(errors),
1714
+ "errors": errors,
1715
+ "config": generation_result['config'],
1716
+ "source": "synthea"
1717
+ }
1718
+
1719
+ except Exception as e:
1720
+ logger.error(f"Error in generate and import Synthea patients: {str(e)}")
1721
+ raise HTTPException(
1722
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1723
+ detail=f"Failed to generate and import Synthea patients: {str(e)}"
1724
+ )
1725
+
1726
  # Export the router as 'patients' for api.__init__.py
1727
  patients = router
api/services/synthea_integration.py ADDED
@@ -0,0 +1,444 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ import os
5
+ import subprocess
6
+ import tempfile
7
+ from datetime import datetime, timedelta
8
+ from pathlib import Path
9
+ from typing import Dict, List, Optional, Any
10
+ import aiofiles
11
+ import aiohttp
12
+ from fastapi import HTTPException, status
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ class SyntheaIntegrationService:
17
+ """
18
+ Service for integrating Synthea FHIR data generation with the CPS application
19
+ """
20
+
21
+ def __init__(self):
22
+ self.base_dir = Path(__file__).resolve().parent.parent.parent
23
+ self.synthea_dir = self.base_dir / "synthea"
24
+ self.output_dir = self.base_dir / "output" / "fhir"
25
+ self.synthea_jar_path = self.synthea_dir / "synthea-with-dependencies.jar"
26
+
27
+ # Ensure directories exist
28
+ self.synthea_dir.mkdir(exist_ok=True)
29
+ self.output_dir.mkdir(parents=True, exist_ok=True)
30
+
31
+ # Synthea configuration
32
+ self.default_config = {
33
+ "population": 10,
34
+ "seed": 42,
35
+ "age_min": 18,
36
+ "age_max": 80,
37
+ "gender": "both", # male, female, both
38
+ "location": "Massachusetts", # Default location
39
+ "modules": ["*"], # All modules
40
+ "exporter": "fhir",
41
+ "exporter.fhir.transaction_bundle": "true",
42
+ "exporter.fhir.include_patient_summary": "true",
43
+ "exporter.fhir.include_encounters": "true",
44
+ "exporter.fhir.include_medications": "true",
45
+ "exporter.fhir.include_conditions": "true",
46
+ "exporter.fhir.include_observations": "true",
47
+ "exporter.fhir.include_procedures": "true",
48
+ "exporter.fhir.include_immunizations": "true",
49
+ "exporter.fhir.include_allergies": "true"
50
+ }
51
+
52
+ async def download_synthea(self) -> bool:
53
+ """
54
+ Download Synthea JAR file if not present
55
+ """
56
+ if self.synthea_jar_path.exists():
57
+ logger.info("✅ Synthea JAR already exists")
58
+ return True
59
+
60
+ try:
61
+ logger.info("📥 Downloading Synthea...")
62
+ synthea_url = "https://github.com/synthetichealth/synthea/releases/download/master-branch-latest/synthea-with-dependencies.jar"
63
+
64
+ async with aiohttp.ClientSession() as session:
65
+ async with session.get(synthea_url) as response:
66
+ if response.status == 200:
67
+ content = await response.read()
68
+ async with aiofiles.open(self.synthea_jar_path, 'wb') as f:
69
+ await f.write(content)
70
+ logger.info("✅ Synthea downloaded successfully")
71
+ return True
72
+ else:
73
+ logger.error(f"❌ Failed to download Synthea: {response.status}")
74
+ return False
75
+ except Exception as e:
76
+ logger.error(f"❌ Error downloading Synthea: {str(e)}")
77
+ return False
78
+
79
+ async def generate_synthea_config(self, config_overrides: Dict[str, Any] = None) -> str:
80
+ """
81
+ Generate Synthea configuration file
82
+ """
83
+ config = self.default_config.copy()
84
+ if config_overrides:
85
+ config.update(config_overrides)
86
+
87
+ config_content = "\n".join([f"{k} = {v}" for k, v in config.items()])
88
+ config_file = self.synthea_dir / "synthea.properties"
89
+
90
+ async with aiofiles.open(config_file, 'w') as f:
91
+ await f.write(config_content)
92
+
93
+ return str(config_file)
94
+
95
+ async def run_synthea_generation(self, config_file: str) -> bool:
96
+ """
97
+ Run Synthea to generate FHIR data
98
+ """
99
+ try:
100
+ logger.info("🚀 Starting Synthea generation...")
101
+
102
+ # Clear output directory
103
+ for file in self.output_dir.glob("*.json"):
104
+ file.unlink()
105
+
106
+ # Run Synthea
107
+ cmd = [
108
+ "java", "-jar", str(self.synthea_jar_path),
109
+ "-c", config_file,
110
+ "-o", str(self.output_dir)
111
+ ]
112
+
113
+ process = await asyncio.create_subprocess_exec(
114
+ *cmd,
115
+ stdout=asyncio.subprocess.PIPE,
116
+ stderr=asyncio.subprocess.PIPE
117
+ )
118
+
119
+ stdout, stderr = await process.communicate()
120
+
121
+ if process.returncode == 0:
122
+ logger.info("✅ Synthea generation completed successfully")
123
+ logger.info(f"Output: {stdout.decode()}")
124
+ return True
125
+ else:
126
+ logger.error(f"❌ Synthea generation failed: {stderr.decode()}")
127
+ return False
128
+
129
+ except Exception as e:
130
+ logger.error(f"❌ Error running Synthea: {str(e)}")
131
+ return False
132
+
133
+ async def process_synthea_output(self) -> List[Dict[str, Any]]:
134
+ """
135
+ Process Synthea output files and convert to application format
136
+ """
137
+ patients = []
138
+
139
+ try:
140
+ # Find all patient files (excluding hospital and practitioner files)
141
+ patient_files = [
142
+ f for f in self.output_dir.glob("*.json")
143
+ if not any(x in f.name for x in ["hospitalInformation", "practitionerInformation"])
144
+ ]
145
+
146
+ logger.info(f"📁 Found {len(patient_files)} patient files")
147
+
148
+ for file_path in patient_files:
149
+ try:
150
+ async with aiofiles.open(file_path, 'r') as f:
151
+ content = await f.read()
152
+ bundle = json.loads(content)
153
+
154
+ patient_data = await self._extract_patient_data(bundle, file_path.name)
155
+ if patient_data:
156
+ patients.append(patient_data)
157
+
158
+ except Exception as e:
159
+ logger.error(f"❌ Error processing {file_path}: {str(e)}")
160
+ continue
161
+
162
+ logger.info(f"✅ Processed {len(patients)} patients from Synthea output")
163
+ return patients
164
+
165
+ except Exception as e:
166
+ logger.error(f"❌ Error processing Synthea output: {str(e)}")
167
+ return []
168
+
169
+ async def _extract_patient_data(self, bundle: Dict[str, Any], filename: str) -> Optional[Dict[str, Any]]:
170
+ """
171
+ Extract patient data from FHIR bundle
172
+ """
173
+ try:
174
+ patient_data = {}
175
+ conditions = []
176
+ medications = []
177
+ encounters = []
178
+ observations = []
179
+ procedures = []
180
+ immunizations = []
181
+ allergies = []
182
+
183
+ if 'entry' not in bundle:
184
+ logger.warning(f"No entries found in bundle: {filename}")
185
+ return None
186
+
187
+ for entry in bundle.get('entry', []):
188
+ resource = entry.get('resource', {})
189
+ resource_type = resource.get('resourceType')
190
+
191
+ if resource_type == 'Patient':
192
+ # Extract patient demographics
193
+ name = resource.get('name', [{}])[0] if resource.get('name') else {}
194
+ address = resource.get('address', [{}])[0] if resource.get('address') else {}
195
+
196
+ patient_data = {
197
+ 'fhir_id': resource.get('id'),
198
+ 'full_name': f"{' '.join(name.get('given', []))} {name.get('family', '')}".strip(),
199
+ 'gender': resource.get('gender', 'unknown'),
200
+ 'date_of_birth': resource.get('birthDate'),
201
+ 'address': ' '.join(address.get('line', [])) if address.get('line') else '',
202
+ 'city': address.get('city', ''),
203
+ 'state': address.get('state', ''),
204
+ 'postal_code': address.get('postalCode', ''),
205
+ 'country': address.get('country', ''),
206
+ 'marital_status': resource.get('maritalStatus', {}).get('text', ''),
207
+ 'language': self._extract_language(resource),
208
+ 'source': 'synthea',
209
+ 'import_date': datetime.utcnow().isoformat(),
210
+ 'last_updated': datetime.utcnow().isoformat()
211
+ }
212
+
213
+ elif resource_type == 'Condition':
214
+ conditions.append({
215
+ 'id': resource.get('id'),
216
+ 'code': resource.get('code', {}).get('text', ''),
217
+ 'status': resource.get('clinicalStatus', {}).get('text', ''),
218
+ 'onset_date': resource.get('onsetDateTime'),
219
+ 'recorded_date': resource.get('recordedDate'),
220
+ 'verification_status': resource.get('verificationStatus', {}).get('text', ''),
221
+ 'category': resource.get('category', [{}])[0].get('text', '') if resource.get('category') else ''
222
+ })
223
+
224
+ elif resource_type == 'MedicationRequest':
225
+ medications.append({
226
+ 'id': resource.get('id'),
227
+ 'name': resource.get('medicationCodeableConcept', {}).get('text', ''),
228
+ 'status': resource.get('status'),
229
+ 'prescribed_date': resource.get('authoredOn'),
230
+ 'requester': resource.get('requester', {}).get('display', ''),
231
+ 'dosage': self._extract_dosage(resource),
232
+ 'intent': resource.get('intent', ''),
233
+ 'priority': resource.get('priority', '')
234
+ })
235
+
236
+ elif resource_type == 'Encounter':
237
+ encounters.append({
238
+ 'id': resource.get('id'),
239
+ 'type': resource.get('type', [{}])[0].get('text', '') if resource.get('type') else '',
240
+ 'status': resource.get('status'),
241
+ 'period': resource.get('period', {}),
242
+ 'service_provider': resource.get('serviceProvider', {}).get('display', ''),
243
+ 'class': resource.get('class', {}).get('code', ''),
244
+ 'reason': resource.get('reasonCode', [{}])[0].get('text', '') if resource.get('reasonCode') else ''
245
+ })
246
+
247
+ elif resource_type == 'Observation':
248
+ observations.append({
249
+ 'id': resource.get('id'),
250
+ 'code': resource.get('code', {}).get('text', ''),
251
+ 'value': self._extract_observation_value(resource),
252
+ 'unit': resource.get('valueQuantity', {}).get('unit', ''),
253
+ 'status': resource.get('status'),
254
+ 'effective_date': resource.get('effectiveDateTime'),
255
+ 'category': resource.get('category', [{}])[0].get('text', '') if resource.get('category') else ''
256
+ })
257
+
258
+ elif resource_type == 'Procedure':
259
+ procedures.append({
260
+ 'id': resource.get('id'),
261
+ 'code': resource.get('code', {}).get('text', ''),
262
+ 'status': resource.get('status'),
263
+ 'performed_date': resource.get('performedDateTime'),
264
+ 'performer': resource.get('performer', [{}])[0].get('actor', {}).get('display', '') if resource.get('performer') else '',
265
+ 'reason': resource.get('reasonCode', [{}])[0].get('text', '') if resource.get('reasonCode') else ''
266
+ })
267
+
268
+ elif resource_type == 'Immunization':
269
+ immunizations.append({
270
+ 'id': resource.get('id'),
271
+ 'vaccine': resource.get('vaccineCode', {}).get('text', ''),
272
+ 'status': resource.get('status'),
273
+ 'date': resource.get('occurrenceDateTime'),
274
+ 'lot_number': resource.get('lotNumber', ''),
275
+ 'expiration_date': resource.get('expirationDate', ''),
276
+ 'manufacturer': resource.get('manufacturer', {}).get('display', '')
277
+ })
278
+
279
+ elif resource_type == 'AllergyIntolerance':
280
+ allergies.append({
281
+ 'id': resource.get('id'),
282
+ 'substance': resource.get('code', {}).get('text', ''),
283
+ 'status': resource.get('clinicalStatus', {}).get('text', ''),
284
+ 'verification_status': resource.get('verificationStatus', {}).get('text', ''),
285
+ 'type': resource.get('type', ''),
286
+ 'category': resource.get('category', []),
287
+ 'reaction': self._extract_allergy_reactions(resource)
288
+ })
289
+
290
+ if patient_data:
291
+ patient_data.update({
292
+ 'conditions': conditions,
293
+ 'medications': medications,
294
+ 'encounters': encounters,
295
+ 'observations': observations,
296
+ 'procedures': procedures,
297
+ 'immunizations': immunizations,
298
+ 'allergies': allergies
299
+ })
300
+
301
+ return patient_data
302
+
303
+ return None
304
+
305
+ except Exception as e:
306
+ logger.error(f"❌ Error extracting patient data from {filename}: {str(e)}")
307
+ return None
308
+
309
+ def _extract_language(self, patient_resource: Dict[str, Any]) -> str:
310
+ """Extract language from patient resource"""
311
+ try:
312
+ communication = patient_resource.get('communication', [])
313
+ if communication and communication[0].get('language'):
314
+ return communication[0]['language'].get('text', 'English')
315
+ return 'English'
316
+ except:
317
+ return 'English'
318
+
319
+ def _extract_dosage(self, medication_resource: Dict[str, Any]) -> str:
320
+ """Extract dosage information from medication resource"""
321
+ try:
322
+ dosage = medication_resource.get('dosageInstruction', [])
323
+ if dosage and dosage[0].get('text'):
324
+ return dosage[0]['text']
325
+ return ''
326
+ except:
327
+ return ''
328
+
329
+ def _extract_observation_value(self, observation_resource: Dict[str, Any]) -> str:
330
+ """Extract observation value"""
331
+ try:
332
+ if 'valueQuantity' in observation_resource:
333
+ value = observation_resource['valueQuantity'].get('value', '')
334
+ unit = observation_resource['valueQuantity'].get('unit', '')
335
+ return f"{value} {unit}".strip()
336
+ elif 'valueCodeableConcept' in observation_resource:
337
+ return observation_resource['valueCodeableConcept'].get('text', '')
338
+ elif 'valueString' in observation_resource:
339
+ return observation_resource['valueString']
340
+ return ''
341
+ except:
342
+ return ''
343
+
344
+ def _extract_allergy_reactions(self, allergy_resource: Dict[str, Any]) -> List[Dict[str, Any]]:
345
+ """Extract allergy reactions"""
346
+ try:
347
+ reactions = allergy_resource.get('reaction', [])
348
+ return [{
349
+ 'manifestation': reaction.get('manifestation', [{}])[0].get('text', '') if reaction.get('manifestation') else '',
350
+ 'severity': reaction.get('severity', ''),
351
+ 'onset': reaction.get('onset', '')
352
+ } for reaction in reactions]
353
+ except:
354
+ return []
355
+
356
+ async def generate_and_import_patients(
357
+ self,
358
+ population: int = 10,
359
+ age_min: int = 18,
360
+ age_max: int = 80,
361
+ gender: str = "both",
362
+ location: str = "Massachusetts"
363
+ ) -> Dict[str, Any]:
364
+ """
365
+ Complete workflow: generate Synthea data and prepare for import
366
+ """
367
+ try:
368
+ logger.info(f"🎯 Starting Synthea generation for {population} patients")
369
+
370
+ # Download Synthea if needed
371
+ if not await self.download_synthea():
372
+ raise HTTPException(
373
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
374
+ detail="Failed to download Synthea"
375
+ )
376
+
377
+ # Generate configuration
378
+ config_overrides = {
379
+ "population": population,
380
+ "age_min": age_min,
381
+ "age_max": age_max,
382
+ "gender": gender,
383
+ "location": location
384
+ }
385
+
386
+ config_file = await self.generate_synthea_config(config_overrides)
387
+
388
+ # Run generation
389
+ if not await self.run_synthea_generation(config_file):
390
+ raise HTTPException(
391
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
392
+ detail="Synthea generation failed"
393
+ )
394
+
395
+ # Process output
396
+ patients = await self.process_synthea_output()
397
+
398
+ return {
399
+ "status": "success",
400
+ "generated_patients": len(patients),
401
+ "patients": patients,
402
+ "config": config_overrides,
403
+ "output_directory": str(self.output_dir)
404
+ }
405
+
406
+ except HTTPException:
407
+ raise
408
+ except Exception as e:
409
+ logger.error(f"❌ Error in generate_and_import_patients: {str(e)}")
410
+ raise HTTPException(
411
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
412
+ detail=f"Synthea integration failed: {str(e)}"
413
+ )
414
+
415
+ async def get_synthea_statistics(self) -> Dict[str, Any]:
416
+ """
417
+ Get statistics about Synthea-generated data
418
+ """
419
+ try:
420
+ stats = {
421
+ "total_files": 0,
422
+ "patient_files": 0,
423
+ "hospital_files": 0,
424
+ "practitioner_files": 0,
425
+ "total_size_mb": 0
426
+ }
427
+
428
+ if self.output_dir.exists():
429
+ for file_path in self.output_dir.glob("*.json"):
430
+ stats["total_files"] += 1
431
+ stats["total_size_mb"] += file_path.stat().st_size / (1024 * 1024)
432
+
433
+ if "hospitalInformation" in file_path.name:
434
+ stats["hospital_files"] += 1
435
+ elif "practitionerInformation" in file_path.name:
436
+ stats["practitioner_files"] += 1
437
+ else:
438
+ stats["patient_files"] += 1
439
+
440
+ return stats
441
+
442
+ except Exception as e:
443
+ logger.error(f"❌ Error getting Synthea statistics: {str(e)}")
444
+ return {"error": str(e)}
requirements.txt CHANGED
@@ -31,4 +31,6 @@ python-docx
31
  pyfcm
32
  httpx
33
  jwt
34
- reportlab>=3.6.0
 
 
 
31
  pyfcm
32
  httpx
33
  jwt
34
+ reportlab>=3.6.0
35
+ aiofiles>=0.8.0
36
+ aiohttp>=3.8.0