Spaces:
Sleeping
Sleeping
Update api/routes.py
Browse files- api/routes.py +56 -47
api/routes.py
CHANGED
|
@@ -135,6 +135,18 @@ def escape_latex_special_chars(text: str) -> str:
|
|
| 135 |
text = text.replace(char, escape)
|
| 136 |
return text
|
| 137 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
async def process_synthea_patient(bundle: dict, file_path: str) -> Optional[dict]:
|
| 139 |
logger.debug(f"Processing patient from file: {file_path}")
|
| 140 |
patient_data = {}
|
|
@@ -569,9 +581,8 @@ async def add_note(
|
|
| 569 |
|
| 570 |
@router.get("/ehr/patients/{patient_id}/pdf", response_class=Response)
|
| 571 |
async def generate_patient_pdf(patient_id: str, current_user: dict = Depends(get_current_user)):
|
| 572 |
-
#
|
| 573 |
-
|
| 574 |
-
logger.setLevel(logging.DEBUG)
|
| 575 |
|
| 576 |
try:
|
| 577 |
if current_user.get('role') not in ['doctor', 'admin']:
|
|
@@ -588,45 +599,48 @@ async def generate_patient_pdf(patient_id: str, current_user: dict = Depends(get
|
|
| 588 |
if not patient:
|
| 589 |
raise HTTPException(status_code=404, detail="Patient not found")
|
| 590 |
|
| 591 |
-
# Format rows safely
|
| 592 |
note_rows = " \\\\\n".join(
|
| 593 |
"{} & {} & {}".format(
|
| 594 |
-
escape_latex_special_chars(n.get("date", "") or ""),
|
| 595 |
-
escape_latex_special_chars(n.get("type", "") or ""),
|
| 596 |
-
escape_latex_special_chars(n.get("text", "") or "")
|
| 597 |
)
|
| 598 |
for n in patient.get("notes", [])
|
| 599 |
-
)
|
|
|
|
| 600 |
condition_rows = " \\\\\n".join(
|
| 601 |
"{} & {} & {} & {} & {}".format(
|
| 602 |
-
escape_latex_special_chars(c.get("id", "") or ""),
|
| 603 |
-
escape_latex_special_chars(c.get("code", "") or ""),
|
| 604 |
-
escape_latex_special_chars(c.get("status", "") or ""),
|
| 605 |
-
escape_latex_special_chars(c.get("onset_date", "") or ""),
|
| 606 |
-
escape_latex_special_chars(c.get("verification_status", "") or "")
|
| 607 |
)
|
| 608 |
for c in patient.get("conditions", [])
|
| 609 |
-
)
|
|
|
|
| 610 |
medication_rows = " \\\\\n".join(
|
| 611 |
"{} & {} & {} & {} & {}".format(
|
| 612 |
-
escape_latex_special_chars(m.get("id", "") or ""),
|
| 613 |
-
escape_latex_special_chars(m.get("name", "") or ""),
|
| 614 |
-
escape_latex_special_chars(m.get("status", "") or ""),
|
| 615 |
-
escape_latex_special_chars(m.get("prescribed_date", "") or ""),
|
| 616 |
-
escape_latex_special_chars(m.get("dosage", "") or "")
|
| 617 |
)
|
| 618 |
for m in patient.get("medications", [])
|
| 619 |
-
)
|
|
|
|
| 620 |
encounter_rows = " \\\\\n".join(
|
| 621 |
"{} & {} & {} & {} & {}".format(
|
| 622 |
-
escape_latex_special_chars(e.get("id", "") or ""),
|
| 623 |
-
escape_latex_special_chars(e.get("type", "") or ""),
|
| 624 |
-
escape_latex_special_chars(e.get("status", "") or ""),
|
| 625 |
-
escape_latex_special_chars(e.get("period", {}).get("start", "") or ""),
|
| 626 |
-
escape_latex_special_chars(e.get("service_provider", "") or "")
|
| 627 |
)
|
| 628 |
for e in patient.get("encounters", [])
|
| 629 |
-
)
|
| 630 |
|
| 631 |
# Use Template for safe insertion
|
| 632 |
latex_template = Template(r"""
|
|
@@ -636,6 +650,8 @@ async def generate_patient_pdf(patient_id: str, current_user: dict = Depends(get
|
|
| 636 |
\usepackage{geometry}
|
| 637 |
\geometry{margin=1in}
|
| 638 |
\usepackage{booktabs,longtable,fancyhdr}
|
|
|
|
|
|
|
| 639 |
\pagestyle{fancy}
|
| 640 |
\fancyhf{}
|
| 641 |
\fancyhead[L]{Patient Report}
|
|
@@ -663,7 +679,7 @@ async def generate_patient_pdf(patient_id: str, current_user: dict = Depends(get
|
|
| 663 |
\end{itemize}
|
| 664 |
|
| 665 |
\section*{Clinical Notes}
|
| 666 |
-
\begin{longtable}{p{3cm}p{3cm}p{7cm}}
|
| 667 |
\toprule
|
| 668 |
\textbf{Date} & \textbf{Type} & \textbf{Text} \\
|
| 669 |
\midrule
|
|
@@ -673,7 +689,7 @@ $notes
|
|
| 673 |
\end{longtable}
|
| 674 |
|
| 675 |
\section*{Conditions}
|
| 676 |
-
\begin{longtable}{p{2cm}p{3cm}p{2cm}p{2cm}p{3cm}}
|
| 677 |
\toprule
|
| 678 |
\textbf{ID} & \textbf{Code} & \textbf{Status} & \textbf{Onset} & \textbf{Verification} \\
|
| 679 |
\midrule
|
|
@@ -683,7 +699,7 @@ $conditions
|
|
| 683 |
\end{longtable}
|
| 684 |
|
| 685 |
\section*{Medications}
|
| 686 |
-
\begin{longtable}{p{2cm}p{4cm}p{2cm}p{2cm}p{2cm}}
|
| 687 |
\toprule
|
| 688 |
\textbf{ID} & \textbf{Name} & \textbf{Status} & \textbf{Date} & \textbf{Dosage} \\
|
| 689 |
\midrule
|
|
@@ -693,7 +709,7 @@ $medications
|
|
| 693 |
\end{longtable}
|
| 694 |
|
| 695 |
\section*{Encounters}
|
| 696 |
-
\begin{longtable}{p{2cm}p{4cm}p{2cm}p{3cm}p{3cm}}
|
| 697 |
\toprule
|
| 698 |
\textbf{ID} & \textbf{Type} & \textbf{Status} & \textbf{Start} & \textbf{Provider} \\
|
| 699 |
\midrule
|
|
@@ -705,12 +721,12 @@ $encounters
|
|
| 705 |
\end{document}
|
| 706 |
""")
|
| 707 |
|
| 708 |
-
# Set the generated_on date to 03:
|
| 709 |
-
generated_on = datetime.strptime("2025-05-16 15:
|
| 710 |
|
| 711 |
latex_filled = latex_template.substitute(
|
| 712 |
generated_on=generated_on,
|
| 713 |
-
fhir_id=escape_latex_special_chars(patient.get("fhir_id", "") or ""),
|
| 714 |
full_name=escape_latex_special_chars(patient.get("full_name", "") or ""),
|
| 715 |
gender=escape_latex_special_chars(patient.get("gender", "") or ""),
|
| 716 |
dob=escape_latex_special_chars(patient.get("date_of_birth", "") or ""),
|
|
@@ -724,10 +740,10 @@ $encounters
|
|
| 724 |
]))),
|
| 725 |
marital_status=escape_latex_special_chars(patient.get("marital_status", "") or ""),
|
| 726 |
language=escape_latex_special_chars(patient.get("language", "") or ""),
|
| 727 |
-
notes=note_rows
|
| 728 |
-
conditions=condition_rows
|
| 729 |
-
medications=medication_rows
|
| 730 |
-
encounters=encounter_rows
|
| 731 |
)
|
| 732 |
|
| 733 |
# Compile LaTeX in a temporary directory
|
|
@@ -739,25 +755,20 @@ $encounters
|
|
| 739 |
f.write(latex_filled)
|
| 740 |
|
| 741 |
try:
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
["latexmk", "-pdf", "-interaction=nonstopmode", "-f", tex_path],
|
| 745 |
cwd=tmpdir,
|
| 746 |
check=True,
|
| 747 |
capture_output=True,
|
| 748 |
text=True
|
| 749 |
)
|
| 750 |
-
logger.debug(f"latexmk stdout: {result.stdout}")
|
| 751 |
-
logger.debug(f"latexmk stderr: {result.stderr}")
|
| 752 |
except subprocess.CalledProcessError as e:
|
| 753 |
-
logger.error(f"LaTeX compilation failed: stdout={e.stdout}, stderr={e.stderr}")
|
| 754 |
raise HTTPException(
|
| 755 |
status_code=500,
|
| 756 |
detail=f"LaTeX compilation failed: stdout={e.stdout}, stderr={e.stderr}"
|
| 757 |
)
|
| 758 |
|
| 759 |
if not os.path.exists(pdf_path):
|
| 760 |
-
logger.error("PDF file was not generated")
|
| 761 |
raise HTTPException(
|
| 762 |
status_code=500,
|
| 763 |
detail="PDF file was not generated"
|
|
@@ -774,17 +785,15 @@ $encounters
|
|
| 774 |
return response
|
| 775 |
|
| 776 |
except HTTPException as http_error:
|
| 777 |
-
logger.error(f"HTTP exception in generate_patient_pdf: {str(http_error.detail)}")
|
| 778 |
raise http_error
|
| 779 |
except Exception as e:
|
| 780 |
-
logger.error(f"Unexpected error generating PDF: {str(e)}", exc_info=True)
|
| 781 |
raise HTTPException(
|
| 782 |
status_code=500,
|
| 783 |
detail=f"Unexpected error generating PDF: {str(e)}"
|
| 784 |
)
|
| 785 |
finally:
|
| 786 |
# Restore the logger level for other routes
|
| 787 |
-
logger.setLevel(
|
| 788 |
|
| 789 |
@router.post("/signup", status_code=status.HTTP_201_CREATED)
|
| 790 |
async def signup(data: SignupForm):
|
|
|
|
| 135 |
text = text.replace(char, escape)
|
| 136 |
return text
|
| 137 |
|
| 138 |
+
def hyphenate_long_strings(text: str, max_length: int = 10) -> str:
|
| 139 |
+
"""Insert LaTeX hyphenation points into long strings to prevent overfull hboxes."""
|
| 140 |
+
if not isinstance(text, str) or len(text) <= max_length:
|
| 141 |
+
return text
|
| 142 |
+
# Insert a discretionary hyphen (\-) every max_length characters
|
| 143 |
+
result = ""
|
| 144 |
+
for i, char in enumerate(text):
|
| 145 |
+
result += char
|
| 146 |
+
if (i + 1) % max_length == 0 and (i + 1) != len(text):
|
| 147 |
+
result += "\\-"
|
| 148 |
+
return result
|
| 149 |
+
|
| 150 |
async def process_synthea_patient(bundle: dict, file_path: str) -> Optional[dict]:
|
| 151 |
logger.debug(f"Processing patient from file: {file_path}")
|
| 152 |
patient_data = {}
|
|
|
|
| 581 |
|
| 582 |
@router.get("/ehr/patients/{patient_id}/pdf", response_class=Response)
|
| 583 |
async def generate_patient_pdf(patient_id: str, current_user: dict = Depends(get_current_user)):
|
| 584 |
+
# Suppress logging for this route
|
| 585 |
+
logger.setLevel(logging.CRITICAL)
|
|
|
|
| 586 |
|
| 587 |
try:
|
| 588 |
if current_user.get('role') not in ['doctor', 'admin']:
|
|
|
|
| 599 |
if not patient:
|
| 600 |
raise HTTPException(status_code=404, detail="Patient not found")
|
| 601 |
|
| 602 |
+
# Format rows safely, ensuring proper column counts and hyphenation
|
| 603 |
note_rows = " \\\\\n".join(
|
| 604 |
"{} & {} & {}".format(
|
| 605 |
+
escape_latex_special_chars(hyphenate_long_strings(n.get("date", "") or "")),
|
| 606 |
+
escape_latex_special_chars(hyphenate_long_strings(n.get("type", "") or "")),
|
| 607 |
+
escape_latex_special_chars(hyphenate_long_strings(n.get("text", "") or ""))
|
| 608 |
)
|
| 609 |
for n in patient.get("notes", [])
|
| 610 |
+
) or "No notes available \\\\"
|
| 611 |
+
|
| 612 |
condition_rows = " \\\\\n".join(
|
| 613 |
"{} & {} & {} & {} & {}".format(
|
| 614 |
+
escape_latex_special_chars(hyphenate_long_strings(c.get("id", "") or "")),
|
| 615 |
+
escape_latex_special_chars(hyphenate_long_strings(c.get("code", "") or "")),
|
| 616 |
+
escape_latex_special_chars(hyphenate_long_strings(c.get("status", "") or "")),
|
| 617 |
+
escape_latex_special_chars(hyphenate_long_strings(c.get("onset_date", "") or "")),
|
| 618 |
+
escape_latex_special_chars(hyphenate_long_strings(c.get("verification_status", "") or ""))
|
| 619 |
)
|
| 620 |
for c in patient.get("conditions", [])
|
| 621 |
+
) or "No conditions available \\\\"
|
| 622 |
+
|
| 623 |
medication_rows = " \\\\\n".join(
|
| 624 |
"{} & {} & {} & {} & {}".format(
|
| 625 |
+
escape_latex_special_chars(hyphenate_long_strings(m.get("id", "") or "")),
|
| 626 |
+
escape_latex_special_chars(hyphenate_long_strings(m.get("name", "") or "")),
|
| 627 |
+
escape_latex_special_chars(hyphenate_long_strings(m.get("status", "") or "")),
|
| 628 |
+
escape_latex_special_chars(hyphenate_long_strings(m.get("prescribed_date", "") or "")),
|
| 629 |
+
escape_latex_special_chars(hyphenate_long_strings(m.get("dosage", "") or ""))
|
| 630 |
)
|
| 631 |
for m in patient.get("medications", [])
|
| 632 |
+
) or "No medications available \\\\"
|
| 633 |
+
|
| 634 |
encounter_rows = " \\\\\n".join(
|
| 635 |
"{} & {} & {} & {} & {}".format(
|
| 636 |
+
escape_latex_special_chars(hyphenate_long_strings(e.get("id", "") or "")),
|
| 637 |
+
escape_latex_special_chars(hyphenate_long_strings(e.get("type", "") or "")),
|
| 638 |
+
escape_latex_special_chars(hyphenate_long_strings(e.get("status", "") or "")),
|
| 639 |
+
escape_latex_special_chars(hyphenate_long_strings(e.get("period", {}).get("start", "") or "")),
|
| 640 |
+
escape_latex_special_chars(hyphenate_long_strings(e.get("service_provider", "") or ""))
|
| 641 |
)
|
| 642 |
for e in patient.get("encounters", [])
|
| 643 |
+
) or "No encounters available \\\\"
|
| 644 |
|
| 645 |
# Use Template for safe insertion
|
| 646 |
latex_template = Template(r"""
|
|
|
|
| 650 |
\usepackage{geometry}
|
| 651 |
\geometry{margin=1in}
|
| 652 |
\usepackage{booktabs,longtable,fancyhdr}
|
| 653 |
+
\usepackage{array} % Added for better table column formatting
|
| 654 |
+
\setlength{\headheight}{14.5pt} % Fix fancyhdr warning
|
| 655 |
\pagestyle{fancy}
|
| 656 |
\fancyhf{}
|
| 657 |
\fancyhead[L]{Patient Report}
|
|
|
|
| 679 |
\end{itemize}
|
| 680 |
|
| 681 |
\section*{Clinical Notes}
|
| 682 |
+
\begin{longtable}{>{\raggedright\arraybackslash}p{3cm}>{\raggedright\arraybackslash}p{3cm}>{\raggedright\arraybackslash}p{7cm}}
|
| 683 |
\toprule
|
| 684 |
\textbf{Date} & \textbf{Type} & \textbf{Text} \\
|
| 685 |
\midrule
|
|
|
|
| 689 |
\end{longtable}
|
| 690 |
|
| 691 |
\section*{Conditions}
|
| 692 |
+
\begin{longtable}{>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{3cm}>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{3cm}}
|
| 693 |
\toprule
|
| 694 |
\textbf{ID} & \textbf{Code} & \textbf{Status} & \textbf{Onset} & \textbf{Verification} \\
|
| 695 |
\midrule
|
|
|
|
| 699 |
\end{longtable}
|
| 700 |
|
| 701 |
\section*{Medications}
|
| 702 |
+
\begin{longtable}{>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{4cm}>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{2cm}}
|
| 703 |
\toprule
|
| 704 |
\textbf{ID} & \textbf{Name} & \textbf{Status} & \textbf{Date} & \textbf{Dosage} \\
|
| 705 |
\midrule
|
|
|
|
| 709 |
\end{longtable}
|
| 710 |
|
| 711 |
\section*{Encounters}
|
| 712 |
+
\begin{longtable}{>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{4cm}>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{3cm}>{\raggedright\arraybackslash}p{3cm}}
|
| 713 |
\toprule
|
| 714 |
\textbf{ID} & \textbf{Type} & \textbf{Status} & \textbf{Start} & \textbf{Provider} \\
|
| 715 |
\midrule
|
|
|
|
| 721 |
\end{document}
|
| 722 |
""")
|
| 723 |
|
| 724 |
+
# Set the generated_on date to 03:52 PM CET, May 16, 2025
|
| 725 |
+
generated_on = datetime.strptime("2025-05-16 15:52:00+01:00", "%Y-%m-%d %H:%M:%S%z").strftime("%A, %B %d, %Y at %I:%M %p")
|
| 726 |
|
| 727 |
latex_filled = latex_template.substitute(
|
| 728 |
generated_on=generated_on,
|
| 729 |
+
fhir_id=escape_latex_special_chars(hyphenate_long_strings(patient.get("fhir_id", "") or "")),
|
| 730 |
full_name=escape_latex_special_chars(patient.get("full_name", "") or ""),
|
| 731 |
gender=escape_latex_special_chars(patient.get("gender", "") or ""),
|
| 732 |
dob=escape_latex_special_chars(patient.get("date_of_birth", "") or ""),
|
|
|
|
| 740 |
]))),
|
| 741 |
marital_status=escape_latex_special_chars(patient.get("marital_status", "") or ""),
|
| 742 |
language=escape_latex_special_chars(patient.get("language", "") or ""),
|
| 743 |
+
notes=note_rows,
|
| 744 |
+
conditions=condition_rows,
|
| 745 |
+
medications=medication_rows,
|
| 746 |
+
encounters=encounter_rows
|
| 747 |
)
|
| 748 |
|
| 749 |
# Compile LaTeX in a temporary directory
|
|
|
|
| 755 |
f.write(latex_filled)
|
| 756 |
|
| 757 |
try:
|
| 758 |
+
subprocess.run(
|
| 759 |
+
["latexmk", "-pdf", "-interaction=nonstopmode", tex_path],
|
|
|
|
| 760 |
cwd=tmpdir,
|
| 761 |
check=True,
|
| 762 |
capture_output=True,
|
| 763 |
text=True
|
| 764 |
)
|
|
|
|
|
|
|
| 765 |
except subprocess.CalledProcessError as e:
|
|
|
|
| 766 |
raise HTTPException(
|
| 767 |
status_code=500,
|
| 768 |
detail=f"LaTeX compilation failed: stdout={e.stdout}, stderr={e.stderr}"
|
| 769 |
)
|
| 770 |
|
| 771 |
if not os.path.exists(pdf_path):
|
|
|
|
| 772 |
raise HTTPException(
|
| 773 |
status_code=500,
|
| 774 |
detail="PDF file was not generated"
|
|
|
|
| 785 |
return response
|
| 786 |
|
| 787 |
except HTTPException as http_error:
|
|
|
|
| 788 |
raise http_error
|
| 789 |
except Exception as e:
|
|
|
|
| 790 |
raise HTTPException(
|
| 791 |
status_code=500,
|
| 792 |
detail=f"Unexpected error generating PDF: {str(e)}"
|
| 793 |
)
|
| 794 |
finally:
|
| 795 |
# Restore the logger level for other routes
|
| 796 |
+
logger.setLevel(logging.INFO)
|
| 797 |
|
| 798 |
@router.post("/signup", status_code=status.HTTP_201_CREATED)
|
| 799 |
async def signup(data: SignupForm):
|