diff --git a/__pycache__/context.cpython-312.pyc b/__pycache__/context.cpython-312.pyc
deleted file mode 100644
index 9989700..0000000
Binary files a/__pycache__/context.cpython-312.pyc and /dev/null differ
diff --git a/app/api.py b/app/api.py
new file mode 100644
index 0000000..c513151
--- /dev/null
+++ b/app/api.py
@@ -0,0 +1,533 @@
+"""
+FastAPI application for report generation with file uploads.
+
+This API allows users to:
+1. Upload required files (Spirometry PDF, Pnoe CSV, SECA Excel)
+2. Generate reports with graphs and analysis
+"""
+
+import shutil
+from pathlib import Path
+from typing import Dict, Optional
+
+import pandas as pd
+from fastapi import FastAPI, File, HTTPException, UploadFile
+from fastapi.responses import FileResponse
+from pydantic import BaseModel
+
+from graph_generator import GraphGenerator
+
+app = FastAPI(
+ title="Medical Report Generation API",
+ description="API for generating medical performance reports with analysis and graphs",
+ version="1.0.0",
+)
+
+# Define upload directory
+UPLOAD_DIR = Path("uploads")
+UPLOAD_DIR.mkdir(exist_ok=True)
+
+# Define output directories
+GRAPHS_DIR = Path("graphs")
+GRAPHS_DIR.mkdir(exist_ok=True)
+REPORTS_DIR = Path("reports")
+REPORTS_DIR.mkdir(exist_ok=True)
+
+# Storage for uploaded files metadata
+uploaded_files_store: Dict[str, Dict[str, str]] = {}
+
+
+class FileUploadResponse(BaseModel):
+ message: str
+ filename: str
+ file_type: str
+ file_path: str
+
+
+class ReportRequest(BaseModel):
+ patient_name: str
+ age: int
+ height: str
+ weight: str
+ focus: str = "Endurance"
+ session_id: Optional[str] = "default"
+
+
+class ReportResponse(BaseModel):
+ message: str
+ report_path: str
+ graphs_generated: list
+ analysis_data: dict
+
+
+@app.get("/")
+async def root():
+ """Root endpoint with API information"""
+ return {
+ "message": "Medical Report Generation API",
+ "version": "1.0.0",
+ "endpoints": {
+ "upload_spirometry": "/upload/spirometry",
+ "upload_pnoe": "/upload/pnoe",
+ "upload_seca": "/upload/seca",
+ "generate_report": "/generate-report",
+ "list_uploads": "/uploads",
+ "health": "/health",
+ },
+ }
+
+
+@app.get("/health")
+async def health_check():
+ """Health check endpoint"""
+ return {"status": "healthy", "service": "report-generation-api"}
+
+
+@app.post("/upload/spirometry", response_model=FileUploadResponse)
+async def upload_spirometry_pdf(
+ file: UploadFile = File(...), session_id: str = "default"
+):
+ """
+ Upload Spirometry PDF file for analysis.
+
+ Args:
+ file: Spirometry PDF file
+ session_id: Session identifier to group files together (default: "default")
+
+ Returns:
+ FileUploadResponse with upload details
+ """
+ if not file.filename.endswith(".pdf"):
+ raise HTTPException(status_code=400, detail="Only PDF files are allowed")
+
+ # Create session directory
+ session_dir = UPLOAD_DIR / session_id
+ session_dir.mkdir(exist_ok=True)
+
+ # Save file
+ file_path = session_dir / f"spirometry_{file.filename}"
+ with open(file_path, "wb") as buffer:
+ shutil.copyfileobj(file.file, buffer)
+
+ # Store metadata
+ if session_id not in uploaded_files_store:
+ uploaded_files_store[session_id] = {}
+
+ uploaded_files_store[session_id]["spirometry_pdf"] = str(file_path)
+
+ return FileUploadResponse(
+ message="Spirometry PDF uploaded successfully",
+ filename=file.filename,
+ file_type="spirometry_pdf",
+ file_path=str(file_path),
+ )
+
+
+@app.post("/upload/pnoe", response_model=FileUploadResponse)
+async def upload_pnoe_csv(file: UploadFile = File(...), session_id: str = "default"):
+ """
+ Upload Pnoe CSV file for metabolic analysis.
+
+ Args:
+ file: Pnoe CSV file
+ session_id: Session identifier to group files together (default: "default")
+
+ Returns:
+ FileUploadResponse with upload details
+ """
+ if not file.filename.endswith(".csv"):
+ raise HTTPException(status_code=400, detail="Only CSV files are allowed")
+
+ # Create session directory
+ session_dir = UPLOAD_DIR / session_id
+ session_dir.mkdir(exist_ok=True)
+
+ # Save file
+ file_path = session_dir / f"pnoe_{file.filename}"
+ with open(file_path, "wb") as buffer:
+ shutil.copyfileobj(file.file, buffer)
+
+ # Store metadata
+ if session_id not in uploaded_files_store:
+ uploaded_files_store[session_id] = {}
+
+ uploaded_files_store[session_id]["pnoe_csv"] = str(file_path)
+
+ return FileUploadResponse(
+ message="Pnoe CSV uploaded successfully",
+ filename=file.filename,
+ file_type="pnoe_csv",
+ file_path=str(file_path),
+ )
+
+
+@app.post("/upload/seca", response_model=FileUploadResponse)
+async def upload_seca_excel(file: UploadFile = File(...), session_id: str = "default"):
+ """
+ Upload SECA body composition Excel file.
+
+ Args:
+ file: SECA Excel file (.xlsx)
+ session_id: Session identifier to group files together (default: "default")
+
+ Returns:
+ FileUploadResponse with upload details
+ """
+ if not file.filename.endswith((".xlsx", ".xls")):
+ raise HTTPException(
+ status_code=400, detail="Only Excel files (.xlsx, .xls) are allowed"
+ )
+
+ # Create session directory
+ session_dir = UPLOAD_DIR / session_id
+ session_dir.mkdir(exist_ok=True)
+
+ # Save file
+ file_path = session_dir / f"seca_{file.filename}"
+ with open(file_path, "wb") as buffer:
+ shutil.copyfileobj(file.file, buffer)
+
+ # Store metadata
+ if session_id not in uploaded_files_store:
+ uploaded_files_store[session_id] = {}
+
+ uploaded_files_store[session_id]["seca_excel"] = str(file_path)
+
+ return FileUploadResponse(
+ message="SECA Excel uploaded successfully",
+ filename=file.filename,
+ file_type="seca_excel",
+ file_path=str(file_path),
+ )
+
+
+@app.get("/uploads")
+async def list_uploads(session_id: str = "default"):
+ """
+ List all uploaded files for a session.
+
+ Args:
+ session_id: Session identifier (default: "default")
+
+ Returns:
+ Dictionary of uploaded files
+ """
+ if session_id not in uploaded_files_store:
+ return {"session_id": session_id, "files": {}, "message": "No files uploaded"}
+
+ return {
+ "session_id": session_id,
+ "files": uploaded_files_store[session_id],
+ "files_count": len(uploaded_files_store[session_id]),
+ }
+
+
+@app.post("/generate-report", response_model=ReportResponse)
+async def generate_report(report_request: ReportRequest):
+ """
+ Generate a comprehensive medical report with graphs and analysis.
+
+ Args:
+ report_request: Report configuration including patient details
+
+ Returns:
+ ReportResponse with report path and analysis data
+ """
+ session_id = report_request.session_id
+
+ # Check if all required files are uploaded
+ if session_id not in uploaded_files_store:
+ raise HTTPException(
+ status_code=400,
+ detail=f"No files found for session '{session_id}'. Please upload files first.",
+ )
+
+ files = uploaded_files_store[session_id]
+ required_files = ["spirometry_pdf", "pnoe_csv", "seca_excel"]
+ missing_files = [f for f in required_files if f not in files]
+
+ if missing_files:
+ raise HTTPException(
+ status_code=400,
+ detail=f"Missing required files: {', '.join(missing_files)}. Please upload all files first.",
+ )
+
+ try:
+ # Initialize graph generator
+ graph_gen = GraphGenerator(charts_dir=str(GRAPHS_DIR))
+
+ # Load and process Pnoe data
+ df = pd.read_csv(files["pnoe_csv"], delimiter=";")
+ df = df.apply(pd.to_numeric, errors="ignore")
+
+ # Calculate derived columns
+ df["VO2 Pulse"] = df["VO2(ml/min)"] / df["HR(bpm)"]
+ df["VO2 Breath"] = df["VO2(ml/min)"] / df["BF(bpm)"]
+ df["CHO"] = df["EE(kcal/min)"] * df["CARBS(%)"] / 100
+ df["FAT"] = df["EE(kcal/min)"] * df["FAT(%)"] / 100
+
+ # Smooth columns
+ window_size = 10
+ columns_to_smooth = [
+ "VO2(ml/min)",
+ "VCO2(ml/min)",
+ "HR(bpm)",
+ "VT(l)",
+ "BF(bpm)",
+ "VE(l/min)",
+ "VO2 Pulse",
+ "VO2 Breath",
+ "CHO",
+ "FAT",
+ ]
+
+ for col in columns_to_smooth:
+ if col in df.columns:
+ df[f"{col}_smoothed"] = (
+ df[col].rolling(window=window_size, min_periods=1).mean()
+ )
+
+ # Generate graphs
+ graphs_generated = []
+
+ # Generate all available graphs from the graph generator
+ try:
+ respiratory_path = graph_gen.generate_respiratory_chart(
+ df, save_as_base64=False
+ )
+ graphs_generated.append(
+ {"name": "respiratory", "path": str(respiratory_path)}
+ )
+ except Exception as e:
+ print(f"Warning: Could not generate respiratory chart: {e}")
+
+ try:
+ fuel_util_path = graph_gen.generate_fuel_utilization_chart(
+ df, save_as_base64=False
+ )
+ graphs_generated.append(
+ {"name": "fuel_utilization", "path": str(fuel_util_path)}
+ )
+ except Exception as e:
+ print(f"Warning: Could not generate fuel utilization chart: {e}")
+
+ try:
+ vo2_pulse_path = graph_gen.generate_vo2_pulse_chart(
+ df, save_as_base64=False
+ )
+ graphs_generated.append({"name": "vo2_pulse", "path": str(vo2_pulse_path)})
+ except Exception as e:
+ print(f"Warning: Could not generate VO2 pulse chart: {e}")
+
+ try:
+ vo2_breath_path = graph_gen.generate_vo2_breath_chart(
+ df, save_as_base64=False
+ )
+ graphs_generated.append(
+ {"name": "vo2_breath", "path": str(vo2_breath_path)}
+ )
+ except Exception as e:
+ print(f"Warning: Could not generate VO2 breath chart: {e}")
+
+ try:
+ fat_metabolism_path = graph_gen.generate_fat_metabolism_chart(
+ df, save_as_base64=False
+ )
+ graphs_generated.append(
+ {"name": "fat_metabolism", "path": str(fat_metabolism_path)}
+ )
+ except Exception as e:
+ print(f"Warning: Could not generate fat metabolism chart: {e}")
+
+ try:
+ recovery_path = graph_gen.generate_recovery_chart(df, save_as_base64=False)
+ graphs_generated.append({"name": "recovery", "path": str(recovery_path)})
+ except Exception as e:
+ print(f"Warning: Could not generate recovery chart: {e}")
+
+ # Calculate basic analysis metrics
+ analysis_data = {
+ "vo2_max": float(df["VO2(ml/min)_smoothed"].max())
+ if "VO2(ml/min)_smoothed" in df.columns
+ else 0,
+ "peak_vt": float(df["VT(l)_smoothed"].max())
+ if "VT(l)_smoothed" in df.columns
+ else 0,
+ "max_hr": float(df["HR(bpm)_smoothed"].max())
+ if "HR(bpm)_smoothed" in df.columns
+ else 0,
+ "graphs_count": len(graphs_generated),
+ }
+
+ # Generate PDF report using existing main.py logic
+ from jinja2 import Environment, FileSystemLoader
+
+ from context import context_list
+ from main import html_string_to_pdf
+
+ env = Environment(loader=FileSystemLoader("report_gen"))
+ html_pages = []
+
+ header_context = {
+ "patient_name": report_request.patient_name,
+ "age": report_request.age,
+ "height": report_request.height,
+ "weight": report_request.weight,
+ "focus": report_request.focus,
+ }
+
+ footer_context = [
+ {
+ "contact_email": "info@ishplabs.com",
+ "website": "www.ishplabs.com",
+ "social": "@ishplabs",
+ "page_number": i + 1,
+ }
+ for i in range(len(context_list))
+ ]
+
+ header_html = env.get_template("header.html").render(header_context)
+ footer_html_list = [
+ env.get_template("footer.html").render(context)
+ for context in footer_context
+ ]
+
+ for i, context in enumerate(context_list):
+ template = env.get_template(f"page_{i + 1}.html").render(context)
+
+ if (i + 1) > 2:
+ full_html = f"""
+
+
+ {header_html}
+
+
+ {template}
+
+
+ {footer_html_list[i]}
+
+
+ """
+ html_pages.append(full_html)
+ else:
+ html_pages.append(template)
+
+ # Combine with page breaks
+ final_html = "".join(html_pages)
+
+ # Wrap in full HTML document
+ html_doc = f"""
+
+
+
+
+
+
+
+
+ {final_html}
+
+
+ """
+
+ # Generate PDF
+ report_filename = (
+ f"report_{report_request.patient_name.replace(' ', '_')}_{session_id}.pdf"
+ )
+ report_path = REPORTS_DIR / report_filename
+ html_string_to_pdf(html_doc, str(report_path))
+
+ return ReportResponse(
+ message="Report generated successfully",
+ report_path=str(report_path),
+ graphs_generated=graphs_generated,
+ analysis_data=analysis_data,
+ )
+
+ except Exception as e:
+ raise HTTPException(
+ status_code=500, detail=f"Error generating report: {str(e)}"
+ )
+
+
+@app.get("/download-report/{filename}")
+async def download_report(filename: str):
+ """
+ Download a generated report.
+
+ Args:
+ filename: Name of the report file
+
+ Returns:
+ PDF file
+ """
+ file_path = REPORTS_DIR / filename
+
+ if not file_path.exists():
+ raise HTTPException(status_code=404, detail="Report not found")
+
+ return FileResponse(
+ path=file_path,
+ media_type="application/pdf",
+ filename=filename,
+ )
+
+
+@app.delete("/uploads/{session_id}")
+async def delete_session_uploads(session_id: str):
+ """
+ Delete all uploaded files for a session.
+
+ Args:
+ session_id: Session identifier
+
+ Returns:
+ Success message
+ """
+ if session_id not in uploaded_files_store:
+ raise HTTPException(status_code=404, detail="Session not found")
+
+ # Delete files
+ session_dir = UPLOAD_DIR / session_id
+ if session_dir.exists():
+ shutil.rmtree(session_dir)
+
+ # Remove from store
+ del uploaded_files_store[session_id]
+
+ return {"message": f"Session '{session_id}' deleted successfully"}
+
+
+if __name__ == "__main__":
+ import uvicorn
+
+ uvicorn.run(app, host="0.0.0.0", port=8000)
diff --git a/graph_generator.py b/app/graph_generator.py
similarity index 100%
rename from graph_generator.py
rename to app/graph_generator.py
diff --git a/report_gen/footer.html b/app/report_gen/footer.html
similarity index 100%
rename from report_gen/footer.html
rename to app/report_gen/footer.html
diff --git a/report_gen/header.html b/app/report_gen/header.html
similarity index 100%
rename from report_gen/header.html
rename to app/report_gen/header.html
diff --git a/report_gen/page_1.html b/app/report_gen/page_1.html
similarity index 100%
rename from report_gen/page_1.html
rename to app/report_gen/page_1.html
diff --git a/report_gen/page_10.html b/app/report_gen/page_10.html
similarity index 100%
rename from report_gen/page_10.html
rename to app/report_gen/page_10.html
diff --git a/report_gen/page_11.html b/app/report_gen/page_11.html
similarity index 100%
rename from report_gen/page_11.html
rename to app/report_gen/page_11.html
diff --git a/report_gen/page_12.html b/app/report_gen/page_12.html
similarity index 100%
rename from report_gen/page_12.html
rename to app/report_gen/page_12.html
diff --git a/report_gen/page_13.html b/app/report_gen/page_13.html
similarity index 100%
rename from report_gen/page_13.html
rename to app/report_gen/page_13.html
diff --git a/report_gen/page_14.html b/app/report_gen/page_14.html
similarity index 100%
rename from report_gen/page_14.html
rename to app/report_gen/page_14.html
diff --git a/report_gen/page_15.html b/app/report_gen/page_15.html
similarity index 100%
rename from report_gen/page_15.html
rename to app/report_gen/page_15.html
diff --git a/report_gen/page_16.html b/app/report_gen/page_16.html
similarity index 100%
rename from report_gen/page_16.html
rename to app/report_gen/page_16.html
diff --git a/report_gen/page_17.html b/app/report_gen/page_17.html
similarity index 100%
rename from report_gen/page_17.html
rename to app/report_gen/page_17.html
diff --git a/report_gen/page_18.html b/app/report_gen/page_18.html
similarity index 100%
rename from report_gen/page_18.html
rename to app/report_gen/page_18.html
diff --git a/report_gen/page_19.html b/app/report_gen/page_19.html
similarity index 100%
rename from report_gen/page_19.html
rename to app/report_gen/page_19.html
diff --git a/report_gen/page_2.html b/app/report_gen/page_2.html
similarity index 100%
rename from report_gen/page_2.html
rename to app/report_gen/page_2.html
diff --git a/report_gen/page_3.html b/app/report_gen/page_3.html
similarity index 100%
rename from report_gen/page_3.html
rename to app/report_gen/page_3.html
diff --git a/report_gen/page_4.html b/app/report_gen/page_4.html
similarity index 100%
rename from report_gen/page_4.html
rename to app/report_gen/page_4.html
diff --git a/report_gen/page_5.html b/app/report_gen/page_5.html
similarity index 100%
rename from report_gen/page_5.html
rename to app/report_gen/page_5.html
diff --git a/report_gen/page_6.html b/app/report_gen/page_6.html
similarity index 100%
rename from report_gen/page_6.html
rename to app/report_gen/page_6.html
diff --git a/report_gen/page_7.html b/app/report_gen/page_7.html
similarity index 100%
rename from report_gen/page_7.html
rename to app/report_gen/page_7.html
diff --git a/report_gen/page_8.html b/app/report_gen/page_8.html
similarity index 100%
rename from report_gen/page_8.html
rename to app/report_gen/page_8.html
diff --git a/report_gen/page_9.html b/app/report_gen/page_9.html
similarity index 100%
rename from report_gen/page_9.html
rename to app/report_gen/page_9.html
diff --git a/analysis.ipynb b/app/services/analysis.ipynb
similarity index 100%
rename from analysis.ipynb
rename to app/services/analysis.ipynb
diff --git a/context.py b/app/services/context.py
similarity index 100%
rename from context.py
rename to app/services/context.py
diff --git a/context_generator.py b/app/services/context_generator.py
similarity index 100%
rename from context_generator.py
rename to app/services/context_generator.py
diff --git a/main.py b/app/services/main.py
similarity index 100%
rename from main.py
rename to app/services/main.py
diff --git a/notebook.ipynb b/app/services/notebook.ipynb
similarity index 100%
rename from notebook.ipynb
rename to app/services/notebook.ipynb
diff --git a/extracted_table.csv b/extracted_table.csv
deleted file mode 100644
index 7482e84..0000000
--- a/extracted_table.csv
+++ /dev/null
@@ -1,12 +0,0 @@
-Parameters,Best,LLN,Pred.,%Pred.,ZScore,PRE#1,PRE#2,PRE#3
-FVC,L,4.24,3.03,3.79,112.0,0.95,4.24,4.17,4.15
-FEV1,L,3.26,2.53,3.16,103.3,0.28,3.26,3.21,3.14
-FEV1/FVC%,76.89,72.47,83.78,91.8,-1.05,76.9,77.0,75.7
-PEF,L/m,684,222,384,178.7,-,444,438,684
-FEF2575,L/s,2.74,2.15,3.42,80.2,-0.84,2.74,2.68,2.48
-FEF25,L/s,6.08,-,-,-,6.08,6.0,5.53
-FEF50,L/s,3.06,-,-,-,3.06,3.1,2.77
-FEF75,L/s,1.06,0.71,1.41,75.1,-0.72,1.06,1.12,0.94
-PEFTime,ms,-,-,79,-,79,49,39
-Evol,mL,-,-,78.0,-,78.0,77.0,197.0
-FEV6,L,4.22,3.03,3.79,111.4,-,4.22,4.17,4.13
\ No newline at end of file
diff --git a/graphs/body_composition_chart.png b/graphs/body_composition_chart.png
deleted file mode 100644
index 337db2f..0000000
Binary files a/graphs/body_composition_chart.png and /dev/null differ
diff --git a/graphs/body_fat_percent_chart.png b/graphs/body_fat_percent_chart.png
deleted file mode 100644
index 3d01923..0000000
Binary files a/graphs/body_fat_percent_chart.png and /dev/null differ
diff --git a/graphs/fat_metabolism_chart.png b/graphs/fat_metabolism_chart.png
deleted file mode 100644
index 6960491..0000000
Binary files a/graphs/fat_metabolism_chart.png and /dev/null differ
diff --git a/graphs/fat_percent_master_chart.png b/graphs/fat_percent_master_chart.png
deleted file mode 100644
index 7e24de3..0000000
Binary files a/graphs/fat_percent_master_chart.png and /dev/null differ
diff --git a/graphs/fuel_utilization_chart.png b/graphs/fuel_utilization_chart.png
deleted file mode 100644
index e475ca3..0000000
Binary files a/graphs/fuel_utilization_chart.png and /dev/null differ
diff --git a/graphs/recovery_chart.png b/graphs/recovery_chart.png
deleted file mode 100644
index 57c6feb..0000000
Binary files a/graphs/recovery_chart.png and /dev/null differ
diff --git a/graphs/respiratory.png b/graphs/respiratory.png
deleted file mode 100644
index ffa0ee6..0000000
Binary files a/graphs/respiratory.png and /dev/null differ
diff --git a/graphs/spirometry_chart.png b/graphs/spirometry_chart.png
deleted file mode 100644
index 0c8b5b5..0000000
Binary files a/graphs/spirometry_chart.png and /dev/null differ
diff --git a/graphs/vo2_breath_chart.png b/graphs/vo2_breath_chart.png
deleted file mode 100644
index 0a5fe3a..0000000
Binary files a/graphs/vo2_breath_chart.png and /dev/null differ
diff --git a/graphs/vo2_pulse_chart.png b/graphs/vo2_pulse_chart.png
deleted file mode 100644
index 2527dea..0000000
Binary files a/graphs/vo2_pulse_chart.png and /dev/null differ
diff --git a/multi_page_report.pdf b/multi_page_report.pdf
deleted file mode 100644
index a54b515..0000000
Binary files a/multi_page_report.pdf and /dev/null differ
diff --git a/requirements.txt b/requirements.txt
index e06511a..0a6b7b8 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,8 @@
+annotated-types==0.7.0
+anyio==4.11.0
asttokens==3.0.0
brotli==1.1.0
+certifi==2025.8.3
cffi==2.0.0
chardet==5.2.0
charset-normalizer==3.4.3
@@ -11,24 +14,39 @@ cssselect2==0.8.0
cycler==0.12.1
debugpy==1.8.17
decorator==5.2.1
+dnspython==2.8.0
+email-validator==2.3.0
et-xmlfile==2.0.0
executing==2.2.1
+fastapi==0.118.0
+fastapi-cli==0.0.13
+fastapi-cloud-cli==0.3.0
fonttools==4.60.0
+greenlet==3.2.4
+h11==0.16.0
+httpcore==1.0.9
+httptools==0.6.4
+httpx==0.28.1
+idna==3.10
ipykernel==6.30.1
ipython==9.5.0
ipython-pygments-lexers==1.1.1
+itsdangerous==2.2.0
jedi==0.19.2
jinja2==3.1.6
jupyter-client==8.6.3
jupyter-core==5.8.1
kiwisolver==1.4.9
+markdown-it-py==4.0.0
markupsafe==3.0.2
matplotlib==3.10.6
matplotlib-inline==0.1.7
+mdurl==0.1.2
nest-asyncio==1.6.0
numpy==2.3.3
opencv-python-headless==4.11.0.86
openpyxl==3.1.5
+orjson==3.11.3
packaging==25.0
pandas==2.3.2
pango==0.0.1
@@ -38,12 +56,18 @@ pdfminer-six==20250506
pexpect==4.9.0
pillow==11.3.0
platformdirs==4.4.0
+playwright==1.55.0
prompt-toolkit==3.0.52
psutil==7.1.0
ptyprocess==0.7.0
pure-eval==0.2.3
pycparser==2.23
+pydantic==2.11.9
+pydantic-core==2.33.2
+pydantic-extra-types==2.10.5
+pydantic-settings==2.11.0
pydyf==0.11.0
+pyee==13.0.0
pygments==2.19.2
pymupdf==1.26.4
pyparsing==3.2.5
@@ -51,17 +75,36 @@ pypdf==5.9.0
pypdfium2==4.30.0
pyphen==0.17.2
python-dateutil==2.9.0.post0
+python-dotenv==1.1.1
+python-multipart==0.0.20
pytz==2025.2
+pyyaml==6.0.3
pyzmq==27.1.0
+rich==14.1.0
+rich-toolkit==0.15.1
+rignore==0.7.0
seaborn==0.13.2
+sentry-sdk==2.39.0
+shellingham==1.5.4
six==1.17.0
+sniffio==1.3.1
stack-data==0.6.3
+starlette==0.48.0
tabulate==0.9.0
tinycss2==1.4.0
tinyhtml5==2.0.0
tornado==6.5.2
traitlets==5.14.3
+typer==0.19.2
+typing-extensions==4.15.0
+typing-inspection==0.4.2
tzdata==2025.2
+ujson==5.11.0
+urllib3==2.5.0
+uvicorn==0.37.0
+uvloop==0.21.0
+watchfiles==1.1.0
wcwidth==0.2.14
webencodings==0.5.1
+websockets==15.0.1
zopfli==0.2.3.post1