Ali2206 commited on
Commit
c808b08
·
verified ·
1 Parent(s): 0c207c3

Update api/routes/patients.py

Browse files
Files changed (1) hide show
  1. api/routes/patients.py +219 -437
api/routes/patients.py CHANGED
@@ -1,23 +1,15 @@
1
- from fastapi import APIRouter, HTTPException, Depends, Query, status
2
  from ...db.mongo import patients_collection
3
  from ...core.security import get_current_user
4
- from ...utils.db import create_indexes
5
- from ...utils.helpers import calculate_age, standardize_language
6
- from ...models.entities import Note
7
  from datetime import datetime
8
  from bson import ObjectId
9
  from bson.errors import InvalidId
10
- from typing import Optional, List, Dict
11
- from pymongo import UpdateOne
12
- from pymongo.errors import BulkWriteError
13
- import json
14
- from pathlib import Path
15
- import glob
16
- import uuid
17
- import re
18
- import logging
19
- import time
20
  import os
 
 
 
 
21
 
22
  # Configure logging
23
  logging.basicConfig(
@@ -28,441 +20,231 @@ logger = logging.getLogger(__name__)
28
 
29
  router = APIRouter()
30
 
31
- # Configuration
32
- BASE_DIR = Path(__file__).resolve().parent.parent.parent
33
- SYNTHEA_DATA_DIR = BASE_DIR / "output" / "fhir"
34
- os.makedirs(SYNTHEA_DATA_DIR, exist_ok=True)
 
 
 
 
35
 
36
- async def process_synthea_patient(bundle: dict, file_path: str) -> Optional[dict]:
37
- logger.debug(f"Processing patient from file: {file_path}")
38
- patient_data = {}
39
- notes = []
40
- conditions = []
41
- medications = []
42
- encounters = []
43
-
44
- # Validate bundle structure
45
- if not isinstance(bundle, dict) or 'entry' not in bundle:
46
- logger.error(f"Invalid FHIR bundle structure in {file_path}")
47
- return None
48
-
49
- for entry in bundle.get('entry', []):
50
- resource = entry.get('resource', {})
51
- resource_type = resource.get('resourceType')
52
-
53
- if not resource_type:
54
- logger.warning(f"Skipping entry with missing resourceType in {file_path}")
55
- continue
56
-
57
  try:
58
- if resource_type == 'Patient':
59
- name = resource.get('name', [{}])[0]
60
- address = resource.get('address', [{}])[0]
61
-
62
- patient_data = {
63
- 'fhir_id': resource.get('id'),
64
- 'full_name': f"{' '.join(name.get('given', ['']))} {name.get('family', '')}".strip(),
65
- 'gender': resource.get('gender', 'unknown'),
66
- 'date_of_birth': resource.get('birthDate', ''),
67
- 'address': ' '.join(address.get('line', [''])),
68
- 'city': address.get('city', ''),
69
- 'state': address.get('state', ''),
70
- 'postal_code': address.get('postalCode', ''),
71
- 'country': address.get('country', ''),
72
- 'marital_status': resource.get('maritalStatus', {}).get('text', ''),
73
- 'language': standardize_language(resource.get('communication', [{}])[0].get('language', {}).get('text', '')),
74
- 'source': 'synthea',
75
- 'last_updated': datetime.utcnow().isoformat()
76
- }
77
-
78
- elif resource_type == 'Encounter':
79
- encounter = {
80
- 'id': resource.get('id'),
81
- 'type': resource.get('type', [{}])[0].get('text', ''),
82
- 'status': resource.get('status'),
83
- 'period': resource.get('period', {}),
84
- 'service_provider': resource.get('serviceProvider', {}).get('display', '')
85
- }
86
- encounters.append(encounter)
87
-
88
- for note in resource.get('note', []):
89
- if note.get('text'):
90
- notes.append({
91
- 'date': resource.get('period', {}).get('start', datetime.utcnow().isoformat()),
92
- 'type': resource.get('type', [{}])[0].get('text', 'Encounter Note'),
93
- 'text': note.get('text'),
94
- 'context': f"Encounter: {encounter.get('type')}",
95
- 'author': 'System Generated'
96
- })
97
-
98
- elif resource_type == 'Condition':
99
- conditions.append({
100
- 'id': resource.get('id'),
101
- 'code': resource.get('code', {}).get('text', ''),
102
- 'status': resource.get('clinicalStatus', {}).get('text', ''),
103
- 'onset_date': resource.get('onsetDateTime'),
104
- 'recorded_date': resource.get('recordedDate'),
105
- 'verification_status': resource.get('verificationStatus', {}).get('text', '')
106
- })
107
-
108
- elif resource_type == 'MedicationRequest':
109
- medications.append({
110
- 'id': resource.get('id'),
111
- 'name': resource.get('medicationCodeableConcept', {}).get('text', ''),
112
- 'status': resource.get('status'),
113
- 'prescribed_date': resource.get('authoredOn'),
114
- 'requester': resource.get('requester', {}).get('display', ''),
115
- 'dosage': resource.get('dosageInstruction', [{}])[0].get('text', '')
116
- })
117
-
118
- except Exception as e:
119
- logger.error(f"Error processing {resource_type} in {file_path}: {str(e)}")
120
- continue
121
-
122
- if patient_data:
123
- patient_data.update({
124
- 'notes': notes,
125
- 'conditions': conditions,
126
- 'medications': medications,
127
- 'encounters': encounters,
128
- 'import_date': datetime.utcnow().isoformat()
129
- })
130
- logger.info(f"Successfully processed patient {patient_data.get('fhir_id')} from {file_path}")
131
- return patient_data
132
- logger.warning(f"No valid patient data found in {file_path}")
133
- return None
134
 
135
- @router.post("/import", status_code=status.HTTP_201_CREATED)
136
- async def import_patients(
137
- limit: int = Query(100, ge=1, le=1000),
138
- current_user: dict = Depends(get_current_user)
139
- ):
140
- request_id = str(uuid.uuid4())
141
- logger.info(f"Starting import request {request_id} by user {current_user.get('email')}")
142
- start_time = time.time()
143
-
144
- if current_user.get('role') not in ['admin', 'doctor']:
145
- logger.warning(f"Unauthorized import attempt by {current_user.get('email')}")
146
- raise HTTPException(
147
- status_code=status.HTTP_403_FORBIDDEN,
148
- detail="Only administrators and doctors can import data"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  )
150
-
151
- try:
152
- await create_indexes()
153
-
154
- if not SYNTHEA_DATA_DIR.exists():
155
- logger.error(f"Synthea data directory not found: {SYNTHEA_DATA_DIR}")
156
- raise HTTPException(
157
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
158
- detail="Data directory not found"
159
- )
160
-
161
- # Filter out non-patient files
162
- files = [
163
- f for f in glob.glob(str(SYNTHEA_DATA_DIR / "*.json"))
164
- if not re.search(r'(hospitalInformation|practitionerInformation)\d+\.json$', f)
165
- ]
166
- if not files:
167
- logger.warning("No valid patient JSON files found in synthea data directory")
168
- return {
169
- "status": "success",
170
- "message": "No patient data files found",
171
- "imported": 0,
172
- "request_id": request_id
173
- }
174
-
175
- operations = []
176
- imported = 0
177
- errors = []
178
-
179
- for file_path in files[:limit]:
180
- try:
181
- logger.debug(f"Processing file: {file_path}")
182
-
183
- # Check file accessibility
184
- if not os.path.exists(file_path):
185
- logger.error(f"File not found: {file_path}")
186
- errors.append(f"File not found: {file_path}")
187
- continue
188
-
189
- # Check file size
190
- file_size = os.path.getsize(file_path)
191
- if file_size == 0:
192
- logger.warning(f"Empty file: {file_path}")
193
- errors.append(f"Empty file: {file_path}")
194
- continue
195
-
196
- with open(file_path, 'r', encoding='utf-8') as f:
197
- try:
198
- bundle = json.load(f)
199
- except json.JSONDecodeError as je:
200
- logger.error(f"Invalid JSON in {file_path}: {str(je)}")
201
- errors.append(f"Invalid JSON in {file_path}: {str(je)}")
202
- continue
203
-
204
- patient = await process_synthea_patient(bundle, file_path)
205
- if patient:
206
- if not patient.get('fhir_id'):
207
- logger.warning(f"Missing FHIR ID in patient data from {file_path}")
208
- errors.append(f"Missing FHIR ID in {file_path}")
209
- continue
210
-
211
- operations.append(UpdateOne(
212
- {"fhir_id": patient['fhir_id']},
213
- {"$setOnInsert": patient},
214
- upsert=True
215
- ))
216
- imported += 1
217
- else:
218
- logger.warning(f"No valid patient data in {file_path}")
219
- errors.append(f"No valid patient data in {file_path}")
220
-
221
- except Exception as e:
222
- logger.error(f"Error processing {file_path}: {str(e)}")
223
- errors.append(f"Error in {file_path}: {str(e)}")
224
- continue
225
-
226
- response = {
227
- "status": "success",
228
- "imported": imported,
229
- "errors": errors,
230
- "request_id": request_id,
231
- "duration_seconds": time.time() - start_time
232
- }
233
-
234
- if operations:
235
  try:
236
- result = await patients_collection.bulk_write(operations, ordered=False)
237
- response.update({
238
- "upserted": result.upserted_count,
239
- "existing": len(operations) - result.upserted_count
240
- })
241
- logger.info(f"Import request {request_id} completed: {imported} patients processed, "
242
- f"{result.upserted_count} upserted, {len(errors)} errors")
243
- except BulkWriteError as bwe:
244
- logger.error(f"Partial bulk write failure for request {request_id}: {str(bwe.details)}")
245
- response.update({
246
- "upserted": bwe.details.get('nUpserted', 0),
247
- "existing": len(operations) - bwe.details.get('nUpserted', 0),
248
- "write_errors": [
249
- f"Index {err['index']}: {err['errmsg']}" for err in bwe.details.get('writeErrors', [])
250
- ]
251
- })
252
- logger.info(f"Import request {request_id} partially completed: {imported} patients processed, "
253
- f"{response['upserted']} upserted, {len(errors)} errors")
254
- except Exception as e:
255
- logger.error(f"Bulk write failed for request {request_id}: {str(e)}")
256
  raise HTTPException(
257
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
258
- detail=f"Database operation failed: {str(e)}"
259
  )
260
- else:
261
- logger.info(f"Import request {request_id} completed: No new patients to import, {len(errors)} errors")
262
- response["message"] = "No new patients found to import"
263
-
264
- return response
265
-
266
- except HTTPException:
267
- raise
268
- except Exception as e:
269
- logger.error(f"Import request {request_id} failed: {str(e)}", exc_info=True)
270
- raise HTTPException(
271
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
272
- detail=f"Import failed: {str(e)}"
273
- )
274
 
275
- @router.get("/patients", response_model=List[dict])
276
- async def list_patients(
277
- search: Optional[str] = Query(None),
278
- min_notes: int = Query(0, ge=0),
279
- min_conditions: int = Query(0, ge=0),
280
- limit: int = Query(100, ge=1, le=500),
281
- skip: int = Query(0, ge=0)
282
- ):
283
- logger.info(f"Listing patients with search: {search}, limit: {limit}, skip: {skip}")
284
- query = {"source": "synthea"}
285
-
286
- if search:
287
- query["$or"] = [
288
- {"full_name": {"$regex": search, "$options": "i"}},
289
- {"fhir_id": search}
290
- ]
291
-
292
- if min_notes > 0:
293
- query[f"notes.{min_notes-1}"] = {"$exists": True}
294
-
295
- if min_conditions > 0:
296
- query[f"conditions.{min_conditions-1}"] = {"$exists": True}
297
-
298
- # Removed $slice to return full arrays for the frontend
299
- projection = {
300
- "fhir_id": 1,
301
- "full_name": 1,
302
- "gender": 1,
303
- "date_of_birth": 1,
304
- "city": 1,
305
- "state": 1,
306
- "conditions": 1,
307
- "medications": 1,
308
- "encounters": 1,
309
- "notes": 1
310
- }
311
-
312
- try:
313
- cursor = patients_collection.find(query, projection).skip(skip).limit(limit)
314
- patients = []
315
-
316
- async for patient in cursor:
317
- patients.append({
318
- "id": str(patient["_id"]),
319
- "fhir_id": patient.get("fhir_id"),
320
- "full_name": patient.get("full_name"),
321
- "gender": patient.get("gender"),
322
- "date_of_birth": patient.get("date_of_birth"),
323
- "city": patient.get("city"),
324
- "state": patient.get("state"),
325
- "conditions": patient.get("conditions", []),
326
- "medications": patient.get("medications", []),
327
- "encounters": patient.get("encounters", []),
328
- "notes": patient.get("notes", []),
329
- "age": calculate_age(patient.get("date_of_birth")),
330
- "stats": {
331
- "notes": len(patient.get("notes", [])),
332
- "conditions": len(patient.get("conditions", [])),
333
- "medications": len(patient.get("medications", []))
334
- }
335
- })
336
-
337
- logger.info(f"Retrieved {len(patients)} patients")
338
- return patients
339
-
340
- except Exception as e:
341
- logger.error(f"Failed to list patients: {str(e)}")
342
- raise HTTPException(
343
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
344
- detail=f"Failed to retrieve patients: {str(e)}"
345
- )
346
 
347
- @router.get("/patients/{patient_id}", response_model=dict)
348
- async def get_patient(patient_id: str):
349
- logger.info(f"Retrieving patient: {patient_id}")
350
- try:
351
- patient = await patients_collection.find_one({
352
- "$or": [
353
- {"_id": ObjectId(patient_id)},
354
- {"fhir_id": patient_id}
355
- ]
356
- })
357
-
358
- if not patient:
359
- logger.warning(f"Patient not found: {patient_id}")
360
- raise HTTPException(
361
- status_code=status.HTTP_404_NOT_FOUND,
362
- detail="Patient not found"
363
- )
364
-
365
- response = {
366
- "demographics": {
367
- "id": str(patient["_id"]),
368
- "fhir_id": patient.get("fhir_id"),
369
- "full_name": patient.get("full_name"),
370
- "gender": patient.get("gender"),
371
- "date_of_birth": patient.get("date_of_birth"),
372
- "age": calculate_age(patient.get("date_of_birth")),
373
- "address": {
374
- "line": patient.get("address"),
375
- "city": patient.get("city"),
376
- "state": patient.get("state"),
377
- "postal_code": patient.get("postal_code"),
378
- "country": patient.get("country")
379
- },
380
- "marital_status": patient.get("marital_status"),
381
- "language": patient.get("language")
382
- },
383
- "clinical_data": {
384
- "notes": patient.get("notes", []),
385
- "conditions": patient.get("conditions", []),
386
- "medications": patient.get("medications", []),
387
- "encounters": patient.get("encounters", [])
388
- },
389
- "metadata": {
390
- "source": patient.get("source"),
391
- "import_date": patient.get("import_date"),
392
- "last_updated": patient.get("last_updated")
393
- }
394
- }
395
-
396
- logger.info(f"Successfully retrieved patient: {patient_id}")
397
- return response
398
-
399
- except ValueError as ve:
400
- logger.error(f"Invalid patient ID format: {patient_id}, error: {str(ve)}")
401
- raise HTTPException(
402
- status_code=status.HTTP_400_BAD_REQUEST,
403
- detail="Invalid patient ID format"
404
- )
405
- except Exception as e:
406
- logger.error(f"Failed to retrieve patient {patient_id}: {str(e)}")
407
- raise HTTPException(
408
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
409
- detail=f"Failed to retrieve patient: {str(e)}"
410
- )
411
 
412
- @router.post("/patients/{patient_id}/notes", status_code=status.HTTP_201_CREATED)
413
- async def add_note(
414
- patient_id: str,
415
- note: Note,
416
- current_user: dict = Depends(get_current_user)
417
- ):
418
- logger.info(f"Adding note for patient {patient_id} by user {current_user.get('email')}")
419
- if current_user.get('role') not in ['doctor', 'admin']:
420
- logger.warning(f"Unauthorized note addition attempt by {current_user.get('email')}")
421
- raise HTTPException(
422
- status_code=status.HTTP_403_FORBIDDEN,
423
- detail="Only clinicians can add notes"
424
- )
425
-
426
- try:
427
- note_data = note.dict()
428
- note_data.update({
429
- "author": current_user.get('full_name', 'System'),
430
- "timestamp": datetime.utcnow().isoformat()
431
- })
432
-
433
- result = await patients_collection.update_one(
434
- {"$or": [
435
- {"_id": ObjectId(patient_id)},
436
- {"fhir_id": patient_id}
437
- ]},
438
- {
439
- "$push": {"notes": note_data},
440
- "$set": {"last_updated": datetime.utcnow().isoformat()}
441
- }
442
- )
443
-
444
- if result.modified_count == 0:
445
- logger.warning(f"Patient not found for note addition: {patient_id}")
446
- raise HTTPException(
447
- status_code=status.HTTP_404_NOT_FOUND,
448
- detail="Patient not found"
449
  )
450
-
451
- logger.info(f"Note added successfully for patient {patient_id}")
452
- return {"status": "success", "message": "Note added"}
453
-
454
- except ValueError as ve:
455
- logger.error(f"Invalid patient ID format: {patient_id}, error: {str(ve)}")
456
- raise HTTPException(
457
- status_code=status.HTTP_400_BAD_REQUEST,
458
- detail="Invalid patient ID format"
459
- )
460
  except Exception as e:
461
- logger.error(f"Failed to add note for patient {patient_id}: {str(e)}")
462
  raise HTTPException(
463
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
464
- detail=f"Failed to add note: {str(e)}"
465
  )
 
 
 
466
 
467
- # Export the router as 'patients' for api.__init__.py
468
- patients = router
 
1
+ from fastapi import APIRouter, HTTPException, Depends, Response
2
  from ...db.mongo import patients_collection
3
  from ...core.security import get_current_user
4
+ from ...utils.helpers import calculate_age, escape_latex_special_chars, hyphenate_long_strings, format_timestamp
 
 
5
  from datetime import datetime
6
  from bson import ObjectId
7
  from bson.errors import InvalidId
 
 
 
 
 
 
 
 
 
 
8
  import os
9
+ import subprocess
10
+ from tempfile import TemporaryDirectory
11
+ from string import Template
12
+ import logging
13
 
14
  # Configure logging
15
  logging.basicConfig(
 
20
 
21
  router = APIRouter()
22
 
23
+ @router.get("/{patient_id}/pdf", response_class=Response)
24
+ async def generate_patient_pdf(patient_id: str, current_user: dict = Depends(get_current_user)):
25
+ # Suppress logging for this route
26
+ logger.setLevel(logging.CRITICAL)
27
+
28
+ try:
29
+ if current_user.get('role') not in ['doctor', 'admin']:
30
+ raise HTTPException(status_code=403, detail="Only clinicians can generate patient PDFs")
31
 
32
+ # Determine if patient_id is ObjectId or fhir_id
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  try:
34
+ obj_id = ObjectId(patient_id)
35
+ query = {"$or": [{"_id": obj_id}, {"fhir_id": patient_id}]}
36
+ except InvalidId:
37
+ query = {"fhir_id": patient_id}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
+ patient = await patients_collection.find_one(query)
40
+ if not patient:
41
+ raise HTTPException(status_code=404, detail="Patient not found")
42
+
43
+ # Prepare table content
44
+ notes = patient.get("notes", [])
45
+ notes_content = ""
46
+ if notes:
47
+ notes_content = "\\toprule\n" + " \\\\\n".join(
48
+ "{} & {} & {}".format(
49
+ escape_latex_special_chars(hyphenate_long_strings(format_timestamp(n.get("date", "") or ""))),
50
+ escape_latex_special_chars(hyphenate_long_strings(n.get("type", "") or "")),
51
+ escape_latex_special_chars(hyphenate_long_strings(n.get("text", "") or ""))
52
+ )
53
+ for n in notes
54
+ ) + "\n\\bottomrule"
55
+ else:
56
+ notes_content = "\\multicolumn{3}{l}{No notes available}"
57
+
58
+ conditions = patient.get("conditions", [])
59
+ conditions_content = ""
60
+ if conditions:
61
+ conditions_content = "\\toprule\n" + " \\\\\n".join(
62
+ "{} & {} & {} & {} & {}".format(
63
+ escape_latex_special_chars(hyphenate_long_strings(c.get("id", "") or "")),
64
+ escape_latex_special_chars(hyphenate_long_strings(c.get("code", "") or "")),
65
+ escape_latex_special_chars(hyphenate_long_strings(c.get("status", "") or "")),
66
+ escape_latex_special_chars(hyphenate_long_strings(format_timestamp(c.get("onset_date", "") or ""))),
67
+ escape_latex_special_chars(hyphenate_long_strings(c.get("verification_status", "") or ""))
68
+ )
69
+ for c in conditions
70
+ ) + "\n\\bottomrule"
71
+ else:
72
+ conditions_content = "\\multicolumn{5}{l}{No conditions available}"
73
+
74
+ medications = patient.get("medications", [])
75
+ medications_content = ""
76
+ if medications:
77
+ medications_content = "\\toprule\n" + " \\\\\n".join(
78
+ "{} & {} & {} & {} & {}".format(
79
+ escape_latex_special_chars(hyphenate_long_strings(m.get("id", "") or "")),
80
+ escape_latex_special_chars(hyphenate_long_strings(m.get("name", "") or "")),
81
+ escape_latex_special_chars(hyphenate_long_strings(m.get("status", "") or "")),
82
+ escape_latex_special_chars(hyphenate_long_strings(format_timestamp(m.get("prescribed_date", "") or ""))),
83
+ escape_latex_special_chars(hyphenate_long_strings(m.get("dosage", "") or ""))
84
+ )
85
+ for m in medications
86
+ ) + "\n\\bottomrule"
87
+ else:
88
+ medications_content = "\\multicolumn{5}{l}{No medications available}"
89
+
90
+ encounters = patient.get("encounters", [])
91
+ encounters_content = ""
92
+ if encounters:
93
+ encounters_content = "\\toprule\n" + " \\\\\n".join(
94
+ "{} & {} & {} & {} & {}".format(
95
+ escape_latex_special_chars(hyphenate_long_strings(e.get("id", "") or "")),
96
+ escape_latex_special_chars(hyphenate_long_strings(e.get("type", "") or "")),
97
+ escape_latex_special_chars(hyphenate_long_strings(e.get("status", "") or "")),
98
+ escape_latex_special_chars(hyphenate_long_strings(format_timestamp(e.get("period", {}).get("start", "") or ""))),
99
+ escape_latex_special_chars(hyphenate_long_strings(e.get("service_provider", "") or ""))
100
+ )
101
+ for e in encounters
102
+ ) + "\n\\bottomrule"
103
+ else:
104
+ encounters_content = "\\multicolumn{5}{l}{No encounters available}"
105
+
106
+ # Use Template for safe insertion
107
+ latex_template = Template(r"""
108
+ \documentclass[a4paper,12pt]{article}
109
+ \usepackage[utf8]{inputenc}
110
+ \usepackage[T1]{fontenc}
111
+ \usepackage{geometry}
112
+ \geometry{margin=1in}
113
+ \usepackage{booktabs,longtable,fancyhdr}
114
+ \usepackage{array}
115
+ \usepackage{microtype}
116
+ \microtypesetup{expansion=false} % Disable font expansion to avoid errors
117
+ \setlength{\headheight}{14.5pt} % Fix fancyhdr warning
118
+ \pagestyle{fancy}
119
+ \fancyhf{}
120
+ \fancyhead[L]{Patient Report}
121
+ \fancyhead[R]{Generated: \today}
122
+ \fancyfoot[C]{\thepage}
123
+
124
+ \begin{document}
125
+
126
+ \begin{center}
127
+ \Large\textbf{Patient Medical Report} \\
128
+ \vspace{0.2cm}
129
+ \textit{Generated on $generated_on}
130
+ \end{center}
131
+
132
+ \section*{Demographics}
133
+ \begin{itemize}
134
+ \item \textbf{FHIR ID:} $fhir_id
135
+ \item \textbf{Full Name:} $full_name
136
+ \item \textbf{Gender:} $gender
137
+ \item \textbf{Date of Birth:} $dob
138
+ \item \textbf{Age:} $age
139
+ \item \textbf{Address:} $address
140
+ \item \textbf{Marital Status:} $marital_status
141
+ \item \textbf{Language:} $language
142
+ \end{itemize}
143
+
144
+ \section*{Clinical Notes}
145
+ \begin{longtable}{>{\raggedright\arraybackslash}p{3.5cm}>{\raggedright\arraybackslash}p{3cm}>{\raggedright\arraybackslash}p{6.5cm}}
146
+ \textbf{Date} & \textbf{Type} & \textbf{Text} \\
147
+ \endhead
148
+ $notes
149
+ \end{longtable}
150
+
151
+ \section*{Conditions}
152
+ \begin{longtable}{>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{3cm}>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{3.5cm}>{\raggedright\arraybackslash}p{3cm}}
153
+ \textbf{ID} & \textbf{Code} & \textbf{Status} & \textbf{Onset} & \textbf{Verification} \\
154
+ \endhead
155
+ $conditions
156
+ \end{longtable}
157
+
158
+ \section*{Medications}
159
+ \begin{longtable}{>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{4cm}>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{3.5cm}>{\raggedright\arraybackslash}p{3cm}}
160
+ \textbf{ID} & \textbf{Name} & \textbf{Status} & \textbf{Date} & \textbf{Dosage} \\
161
+ \endhead
162
+ $medications
163
+ \end{longtable}
164
+
165
+ \section*{Encounters}
166
+ \begin{longtable}{>{\raggedright\arraybackslash}p{2.5cm}>{\raggedright\arraybackslash}p{4.5cm}>{\raggedright\arraybackslash}p{2.5cm}>{\raggedright\arraybackslash}p{4.5cm}>{\raggedright\arraybackslash}p{3.5cm}}
167
+ \textbf{ID} & \textbf{Type} & \textbf{Status} & \textbf{Start} & \textbf{Provider} \\
168
+ \endhead
169
+ $encounters
170
+ \end{longtable}
171
+
172
+ \end{document}
173
+ """)
174
+
175
+ # Set the generated_on date to 06:07 PM CET, May 16, 2025 (based on system note)
176
+ generated_on = datetime.strptime("2025-05-16 18:07:00+02:00", "%Y-%m-%d %H:%M:%S%z").strftime("%A, %B %d, %Y at %I:%M %p %Z")
177
+
178
+ latex_filled = latex_template.substitute(
179
+ generated_on=generated_on,
180
+ fhir_id=escape_latex_special_chars(hyphenate_long_strings(patient.get("fhir_id", "") or "")),
181
+ full_name=escape_latex_special_chars(patient.get("full_name", "") or ""),
182
+ gender=escape_latex_special_chars(patient.get("gender", "") or ""),
183
+ dob=escape_latex_special_chars(patient.get("date_of_birth", "") or ""),
184
+ age=escape_latex_special_chars(str(calculate_age(patient.get("date_of_birth", "")) or "N/A")),
185
+ address=escape_latex_special_chars(", ".join(filter(None, [
186
+ patient.get("address", ""),
187
+ patient.get("city", ""),
188
+ patient.get("state", ""),
189
+ patient.get("postal_code", ""),
190
+ patient.get("country", "")
191
+ ]))),
192
+ marital_status=escape_latex_special_chars(patient.get("marital_status", "") or ""),
193
+ language=escape_latex_special_chars(patient.get("language", "") or ""),
194
+ notes=notes_content,
195
+ conditions=conditions_content,
196
+ medications=medications_content,
197
+ encounters=encounters_content
198
  )
199
+
200
+ # Compile LaTeX in a temporary directory
201
+ with TemporaryDirectory() as tmpdir:
202
+ tex_path = os.path.join(tmpdir, "report.tex")
203
+ pdf_path = os.path.join(tmpdir, "report.pdf")
204
+
205
+ with open(tex_path, "w", encoding="utf-8") as f:
206
+ f.write(latex_filled)
207
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  try:
209
+ subprocess.run(
210
+ ["latexmk", "-pdf", "-interaction=nonstopmode", tex_path],
211
+ cwd=tmpdir,
212
+ check=True,
213
+ capture_output=True,
214
+ text=True
215
+ )
216
+ except subprocess.CalledProcessError as e:
 
 
 
 
 
 
 
 
 
 
 
 
217
  raise HTTPException(
218
+ status_code=500,
219
+ detail=f"LaTeX compilation failed: stdout={e.stdout}, stderr={e.stderr}"
220
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
 
222
+ if not os.path.exists(pdf_path):
223
+ raise HTTPException(
224
+ status_code=500,
225
+ detail="PDF file was not generated"
226
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
 
228
+ with open(pdf_path, "rb") as f:
229
+ pdf_bytes = f.read()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
 
231
+ response = Response(
232
+ content=pdf_bytes,
233
+ media_type="application/pdf",
234
+ headers={"Content-Disposition": f"attachment; filename=patient_{patient.get('fhir_id', 'unknown')}_report.pdf"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  )
236
+ return response
237
+
238
+ except HTTPException as http_error:
239
+ raise http_error
 
 
 
 
 
 
240
  except Exception as e:
 
241
  raise HTTPException(
242
+ status_code=500,
243
+ detail=f"Unexpected error generating PDF: {str(e)}"
244
  )
245
+ finally:
246
+ # Restore the logger level for other routes
247
+ logger.setLevel(logging.INFO)
248
 
249
+ # Export the router as 'pdf' for api.__init__.py
250
+ pdf = router