Spaces:
Sleeping
Sleeping
Update api/routes.py
Browse files- api/routes.py +105 -30
api/routes.py
CHANGED
|
@@ -5,6 +5,7 @@ from db.mongo import users_collection, patients_collection, appointments_collect
|
|
| 5 |
from core.security import hash_password, verify_password, create_access_token, get_current_user
|
| 6 |
from datetime import datetime, timedelta
|
| 7 |
from bson import ObjectId
|
|
|
|
| 8 |
from typing import Optional, List, Dict
|
| 9 |
from pydantic import BaseModel, Field
|
| 10 |
from pymongo import UpdateOne, InsertOne, IndexModel
|
|
@@ -113,6 +114,26 @@ def standardize_language(language: str) -> str:
|
|
| 113 |
return 'en' # Default to English
|
| 114 |
return LANGUAGE_MAP.get(language, 'en')
|
| 115 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
async def process_synthea_patient(bundle: dict, file_path: str) -> Optional[dict]:
|
| 117 |
logger.debug(f"Processing patient from file: {file_path}")
|
| 118 |
patient_data = {}
|
|
@@ -640,64 +661,110 @@ async def generate_patient_pdf(patient_id: str, current_user: dict = Depends(get
|
|
| 640 |
\end{document}
|
| 641 |
"""
|
| 642 |
|
| 643 |
-
# Build the LaTeX-safe rows
|
| 644 |
notes = "\n".join([
|
| 645 |
-
"{} & {} & {} \\\\".format(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 646 |
for n in patient.get("notes", [])
|
| 647 |
])
|
| 648 |
conditions = "\n".join([
|
| 649 |
"{} & {} & {} & {} & {} \\\\".format(
|
| 650 |
-
c.get("id", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 651 |
)
|
| 652 |
for c in patient.get("conditions", [])
|
| 653 |
])
|
| 654 |
medications = "\n".join([
|
| 655 |
"{} & {} & {} & {} & {} \\\\".format(
|
| 656 |
-
m.get("id", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 657 |
)
|
| 658 |
for m in patient.get("medications", [])
|
| 659 |
])
|
| 660 |
encounters = "\n".join([
|
| 661 |
"{} & {} & {} & {} & {} \\\\".format(
|
| 662 |
-
e.get("id", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 663 |
)
|
| 664 |
for e in patient.get("encounters", [])
|
| 665 |
])
|
| 666 |
|
| 667 |
-
# Update the generated_on date to reflect the current time: 03:
|
| 668 |
latex_filled = latex_template % {
|
| 669 |
-
"generated_on": datetime.strptime("2025-05-16 15:
|
| 670 |
-
"fhir_id": patient.get("fhir_id", ""),
|
| 671 |
-
"full_name": patient.get("full_name", ""),
|
| 672 |
-
"gender": patient.get("gender", ""),
|
| 673 |
-
"dob": patient.get("date_of_birth", ""),
|
| 674 |
-
"age": calculate_age(patient.get("date_of_birth", "")) or "N/A",
|
| 675 |
-
"address": "{}, {}, {}, {}, {}".format(
|
| 676 |
-
patient.get("address", ""),
|
| 677 |
-
patient.get("
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
|
|
|
|
|
|
|
|
|
| 681 |
"notes": notes or "No notes available \\\\",
|
| 682 |
"conditions": conditions or "No conditions available \\\\",
|
| 683 |
"medications": medications or "No medications available \\\\",
|
| 684 |
"encounters": encounters or "No encounters available \\\\"
|
| 685 |
}
|
| 686 |
|
| 687 |
-
# Write LaTeX to file
|
| 688 |
-
with NamedTemporaryFile(suffix=".tex", mode="w", delete=False) as tex_file:
|
| 689 |
-
tex_file.write(latex_filled)
|
| 690 |
-
tex_path = tex_file.name
|
| 691 |
-
|
| 692 |
try:
|
| 693 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 694 |
pdf_path = tex_path.replace(".tex", ".pdf")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 695 |
with open(pdf_path, "rb") as f:
|
| 696 |
pdf_bytes = f.read()
|
| 697 |
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 701 |
|
| 702 |
return Response(
|
| 703 |
content=pdf_bytes,
|
|
@@ -705,11 +772,19 @@ async def generate_patient_pdf(patient_id: str, current_user: dict = Depends(get
|
|
| 705 |
headers={"Content-Disposition": f"attachment; filename=patient_{patient.get('fhir_id', 'unknown')}_record.pdf"}
|
| 706 |
)
|
| 707 |
|
| 708 |
-
except
|
| 709 |
-
raise HTTPException(
|
|
|
|
|
|
|
|
|
|
| 710 |
|
|
|
|
|
|
|
| 711 |
except Exception as e:
|
| 712 |
-
raise HTTPException(
|
|
|
|
|
|
|
|
|
|
| 713 |
finally:
|
| 714 |
# Restore the logger level for other routes
|
| 715 |
logger.setLevel(logging.INFO)
|
|
|
|
| 5 |
from core.security import hash_password, verify_password, create_access_token, get_current_user
|
| 6 |
from datetime import datetime, timedelta
|
| 7 |
from bson import ObjectId
|
| 8 |
+
from bson.errors import InvalidId # Added to fix the 'InvalidId' not defined error
|
| 9 |
from typing import Optional, List, Dict
|
| 10 |
from pydantic import BaseModel, Field
|
| 11 |
from pymongo import UpdateOne, InsertOne, IndexModel
|
|
|
|
| 114 |
return 'en' # Default to English
|
| 115 |
return LANGUAGE_MAP.get(language, 'en')
|
| 116 |
|
| 117 |
+
def escape_latex_special_chars(text: str) -> str:
|
| 118 |
+
"""Escape special LaTeX characters to prevent compilation errors."""
|
| 119 |
+
if not isinstance(text, str):
|
| 120 |
+
return ""
|
| 121 |
+
replacements = {
|
| 122 |
+
"&": "\\&",
|
| 123 |
+
"%": "\\%",
|
| 124 |
+
"$": "\\$",
|
| 125 |
+
"#": "\\#",
|
| 126 |
+
"_": "\\_",
|
| 127 |
+
"{": "\\{",
|
| 128 |
+
"}": "\\}",
|
| 129 |
+
"~": "\\textasciitilde{}",
|
| 130 |
+
"^": "\\textasciicircum{}",
|
| 131 |
+
"\\": "\\textbackslash{}"
|
| 132 |
+
}
|
| 133 |
+
for char, escape in replacements.items():
|
| 134 |
+
text = text.replace(char, escape)
|
| 135 |
+
return text
|
| 136 |
+
|
| 137 |
async def process_synthea_patient(bundle: dict, file_path: str) -> Optional[dict]:
|
| 138 |
logger.debug(f"Processing patient from file: {file_path}")
|
| 139 |
patient_data = {}
|
|
|
|
| 661 |
\end{document}
|
| 662 |
"""
|
| 663 |
|
| 664 |
+
# Build the LaTeX-safe rows with escaped special characters
|
| 665 |
notes = "\n".join([
|
| 666 |
+
"{} & {} & {} \\\\".format(
|
| 667 |
+
escape_latex_special_chars(n.get("date", "")),
|
| 668 |
+
escape_latex_special_chars(n.get("type", "")),
|
| 669 |
+
escape_latex_special_chars(n.get("text", ""))
|
| 670 |
+
)
|
| 671 |
for n in patient.get("notes", [])
|
| 672 |
])
|
| 673 |
conditions = "\n".join([
|
| 674 |
"{} & {} & {} & {} & {} \\\\".format(
|
| 675 |
+
escape_latex_special_chars(c.get("id", "")),
|
| 676 |
+
escape_latex_special_chars(c.get("code", "")),
|
| 677 |
+
escape_latex_special_chars(c.get("status", "")),
|
| 678 |
+
escape_latex_special_chars(c.get("onset_date", "")),
|
| 679 |
+
escape_latex_special_chars(c.get("verification_status", ""))
|
| 680 |
)
|
| 681 |
for c in patient.get("conditions", [])
|
| 682 |
])
|
| 683 |
medications = "\n".join([
|
| 684 |
"{} & {} & {} & {} & {} \\\\".format(
|
| 685 |
+
escape_latex_special_chars(m.get("id", "")),
|
| 686 |
+
escape_latex_special_chars(m.get("name", "")),
|
| 687 |
+
escape_latex_special_chars(m.get("status", "")),
|
| 688 |
+
escape_latex_special_chars(m.get("prescribed_date", "")),
|
| 689 |
+
escape_latex_special_chars(m.get("dosage", ""))
|
| 690 |
)
|
| 691 |
for m in patient.get("medications", [])
|
| 692 |
])
|
| 693 |
encounters = "\n".join([
|
| 694 |
"{} & {} & {} & {} & {} \\\\".format(
|
| 695 |
+
escape_latex_special_chars(e.get("id", "")),
|
| 696 |
+
escape_latex_special_chars(e.get("type", "")),
|
| 697 |
+
escape_latex_special_chars(e.get("status", "")),
|
| 698 |
+
escape_latex_special_chars(e.get("period", {}).get("start", "")),
|
| 699 |
+
escape_latex_special_chars(e.get("service_provider", ""))
|
| 700 |
)
|
| 701 |
for e in patient.get("encounters", [])
|
| 702 |
])
|
| 703 |
|
| 704 |
+
# Update the generated_on date to reflect the current time: 03:14 PM CET, May 16, 2025
|
| 705 |
latex_filled = latex_template % {
|
| 706 |
+
"generated_on": datetime.strptime("2025-05-16 15:14:00+01:00", "%Y-%m-%d %H:%M:%S%z").strftime("%A, %B %d, %Y at %I:%M %p"),
|
| 707 |
+
"fhir_id": escape_latex_special_chars(patient.get("fhir_id", "")),
|
| 708 |
+
"full_name": escape_latex_special_chars(patient.get("full_name", "")),
|
| 709 |
+
"gender": escape_latex_special_chars(patient.get("gender", "")),
|
| 710 |
+
"dob": escape_latex_special_chars(patient.get("date_of_birth", "")),
|
| 711 |
+
"age": escape_latex_special_chars(str(calculate_age(patient.get("date_of_birth", "")) or "N/A")),
|
| 712 |
+
"address": escape_latex_special_chars("{}, {}, {}, {}, {}".format(
|
| 713 |
+
patient.get("address", ""),
|
| 714 |
+
patient.get("city", ""),
|
| 715 |
+
patient.get("state", ""),
|
| 716 |
+
patient.get("postal_code", ""),
|
| 717 |
+
patient.get("country", "")
|
| 718 |
+
)),
|
| 719 |
+
"marital_status": escape_latex_special_chars(patient.get("marital_status", "")),
|
| 720 |
+
"language": escape_latex_special_chars(patient.get("language", "")),
|
| 721 |
"notes": notes or "No notes available \\\\",
|
| 722 |
"conditions": conditions or "No conditions available \\\\",
|
| 723 |
"medications": medications or "No medications available \\\\",
|
| 724 |
"encounters": encounters or "No encounters available \\\\"
|
| 725 |
}
|
| 726 |
|
| 727 |
+
# Write LaTeX to file with error handling
|
|
|
|
|
|
|
|
|
|
|
|
|
| 728 |
try:
|
| 729 |
+
with NamedTemporaryFile(suffix=".tex", mode="w", delete=False, encoding="utf-8") as tex_file:
|
| 730 |
+
tex_file.write(latex_filled)
|
| 731 |
+
tex_path = tex_file.name
|
| 732 |
+
|
| 733 |
+
# Compile LaTeX to PDF
|
| 734 |
+
try:
|
| 735 |
+
result = subprocess.run(
|
| 736 |
+
["latexmk", "-pdf", tex_path],
|
| 737 |
+
check=True,
|
| 738 |
+
capture_output=True,
|
| 739 |
+
text=True
|
| 740 |
+
)
|
| 741 |
+
except subprocess.CalledProcessError as e:
|
| 742 |
+
raise HTTPException(
|
| 743 |
+
status_code=500,
|
| 744 |
+
detail=f"LaTeX compilation failed: {e.stderr}"
|
| 745 |
+
)
|
| 746 |
+
|
| 747 |
+
# Read the generated PDF
|
| 748 |
pdf_path = tex_path.replace(".tex", ".pdf")
|
| 749 |
+
if not os.path.exists(pdf_path):
|
| 750 |
+
raise HTTPException(
|
| 751 |
+
status_code=500,
|
| 752 |
+
detail="PDF file was not generated"
|
| 753 |
+
)
|
| 754 |
+
|
| 755 |
with open(pdf_path, "rb") as f:
|
| 756 |
pdf_bytes = f.read()
|
| 757 |
|
| 758 |
+
# Clean up temporary files
|
| 759 |
+
try:
|
| 760 |
+
subprocess.run(["latexmk", "-c", tex_path], check=True, capture_output=True)
|
| 761 |
+
os.remove(tex_path)
|
| 762 |
+
os.remove(pdf_path)
|
| 763 |
+
except Exception as cleanup_error:
|
| 764 |
+
raise HTTPException(
|
| 765 |
+
status_code=500,
|
| 766 |
+
detail=f"Failed to clean up temporary files: {str(cleanup_error)}"
|
| 767 |
+
)
|
| 768 |
|
| 769 |
return Response(
|
| 770 |
content=pdf_bytes,
|
|
|
|
| 772 |
headers={"Content-Disposition": f"attachment; filename=patient_{patient.get('fhir_id', 'unknown')}_record.pdf"}
|
| 773 |
)
|
| 774 |
|
| 775 |
+
except IOError as io_error:
|
| 776 |
+
raise HTTPException(
|
| 777 |
+
status_code=500,
|
| 778 |
+
detail=f"Failed to write LaTeX file: {str(io_error)}"
|
| 779 |
+
)
|
| 780 |
|
| 781 |
+
except HTTPException as http_error:
|
| 782 |
+
raise http_error
|
| 783 |
except Exception as e:
|
| 784 |
+
raise HTTPException(
|
| 785 |
+
status_code=500,
|
| 786 |
+
detail=f"Failed to generate PDF: {str(e)}"
|
| 787 |
+
)
|
| 788 |
finally:
|
| 789 |
# Restore the logger level for other routes
|
| 790 |
logger.setLevel(logging.INFO)
|