Avinashnalla7 commited on
Commit
bcf3c84
·
1 Parent(s): ac909e8

Email confirmation on send-config

Browse files
Files changed (2) hide show
  1. README.md +11 -0
  2. backend/api.py +140 -10
README.md CHANGED
@@ -9,3 +9,14 @@ pinned: false
9
  ---
10
 
11
  FastAPI service for PDF Trainer (pdf upload/download + send-config + notify-unknown).
 
 
 
 
 
 
 
 
 
 
 
 
9
  ---
10
 
11
  FastAPI service for PDF Trainer (pdf upload/download + send-config + notify-unknown).
12
+
13
+ ## Save configuration email
14
+
15
+ When the Trainer UI calls `POST /api/send-config`, the API stores the JSON config and (optionally) emails a confirmation
16
+ with the config JSON + original PDF attached.
17
+
18
+ Set these env vars on the **pdf-trainer-api Space**:
19
+
20
+ - `PDF_PIPELINE_PIPELINE_NOTIFY_TO` (recipient email for confirmations)
21
+ - `PDF_PIPELINE_NOTIFY_FROM` (must be the same Gmail account you OAuth’d)
22
+ - `GMAIL_CREDENTIALS_JSON` + `GMAIL_TOKEN_JSON` (paste full JSON contents; or use `PDF_PIPELINE_GMAIL_CREDENTIALS_JSON` / `PDF_PIPELINE_GMAIL_TOKEN_JSON`)
backend/api.py CHANGED
@@ -8,6 +8,8 @@ from typing import Any, Dict, Optional
8
  from fastapi import FastAPI, UploadFile, File, Form, HTTPException
9
  from fastapi.responses import FileResponse, JSONResponse
10
 
 
 
11
  app = FastAPI(title="PDF Trainer API", version="1.0")
12
 
13
  DATA_DIR = Path(os.environ.get("DATA_DIR", "/data/uploads")).resolve()
@@ -16,6 +18,107 @@ CFG_DIR = DATA_DIR / "configs"
16
  PDF_DIR.mkdir(parents=True, exist_ok=True)
17
  CFG_DIR.mkdir(parents=True, exist_ok=True)
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
  @app.get("/health")
21
  async def health() -> Dict[str, Any]:
@@ -83,6 +186,7 @@ async def send_config(payload: Dict[str, Any]) -> Dict[str, Any]:
83
  pdf_id = (payload.get("pdf_id") or "").strip()
84
  template_id = (payload.get("template_id") or "").strip()
85
  config = payload.get("config")
 
86
 
87
  if not pdf_id:
88
  raise HTTPException(status_code=400, detail="Missing pdf_id")
@@ -99,21 +203,47 @@ async def send_config(payload: Dict[str, Any]) -> Dict[str, Any]:
99
  except Exception as e:
100
  raise HTTPException(status_code=500, detail=f"failed to store config: {e}")
101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  pdf_exists = (PDF_DIR / f"{pdf_id}.pdf").exists()
103
- return {"ok": True, "stored": str(cfg_path), "pdf_exists": pdf_exists, "sftp": "not_implemented_here"}
104
 
105
- @app.put("/api/pdf/{pdf_id}")
106
- async def put_pdf_alias(
107
- pdf_id: str,
108
- file: UploadFile = File(...),
109
- pdf_name: str = Form("")
110
- ):
111
- return await post_pdf(pdf_id=pdf_id, file=file, pdf_name=pdf_name)
 
 
 
 
 
 
 
 
 
112
 
113
  @app.put("/api/pdf/{pdf_id}")
114
  async def put_pdf_alias(
115
  pdf_id: str,
116
  file: UploadFile = File(...),
117
- pdf_name: str = Form("")
118
  ):
119
- return await post_pdf(pdf_id=pdf_id, file=file, pdf_name=pdf_name)
 
 
8
  from fastapi import FastAPI, UploadFile, File, Form, HTTPException
9
  from fastapi.responses import FileResponse, JSONResponse
10
 
11
+ from backend.worker.gmail_client import GmailClient
12
+
13
  app = FastAPI(title="PDF Trainer API", version="1.0")
14
 
15
  DATA_DIR = Path(os.environ.get("DATA_DIR", "/data/uploads")).resolve()
 
18
  PDF_DIR.mkdir(parents=True, exist_ok=True)
19
  CFG_DIR.mkdir(parents=True, exist_ok=True)
20
 
21
+ _MAX_EMAIL_ATTACHMENT_BYTES = 20 * 1024 * 1024 # ~20MB
22
+
23
+
24
+ def _looks_like_json(s: str) -> bool:
25
+ s = (s or "").strip()
26
+ return s.startswith("{") and s.endswith("}")
27
+
28
+
29
+ def _alias_env(primary: str, fallback: str) -> None:
30
+ if (os.environ.get(primary) or "").strip():
31
+ return
32
+ fb = (os.environ.get(fallback) or "").strip()
33
+ if fb:
34
+ os.environ[primary] = fb
35
+
36
+
37
+ def _resolve_json_or_path(env_name: str, default_path: Path, out_path: Path) -> Path:
38
+ raw = (os.environ.get(env_name) or "").strip()
39
+ if not raw:
40
+ return default_path
41
+
42
+ if _looks_like_json(raw):
43
+ out_path.parent.mkdir(parents=True, exist_ok=True)
44
+ out_path.write_text(raw, encoding="utf-8")
45
+ return out_path
46
+
47
+ return Path(raw)
48
+
49
+
50
+ def _gmail_client() -> GmailClient:
51
+ _alias_env("GMAIL_CREDENTIALS_JSON", "PDF_PIPELINE_GMAIL_CREDENTIALS_JSON")
52
+ _alias_env("GMAIL_TOKEN_JSON", "PDF_PIPELINE_GMAIL_TOKEN_JSON")
53
+
54
+ creds_path = _resolve_json_or_path(
55
+ "GMAIL_CREDENTIALS_JSON",
56
+ Path("backend/credentials.json"),
57
+ Path("/tmp/credentials.json"),
58
+ )
59
+ token_path = _resolve_json_or_path(
60
+ "GMAIL_TOKEN_JSON",
61
+ Path("backend/token.json"),
62
+ Path("/tmp/token.json"),
63
+ )
64
+ return GmailClient(creds_path, token_path)
65
+
66
+
67
+ def _send_config_confirmation_email(
68
+ *,
69
+ pdf_id: str,
70
+ template_id: str,
71
+ config_obj: Any,
72
+ notify_to: str,
73
+ ) -> None:
74
+ notify_from = (os.environ.get("PDF_PIPELINE_NOTIFY_FROM") or "").strip()
75
+ if not notify_from:
76
+ raise RuntimeError("Missing PDF_PIPELINE_NOTIFY_FROM env var")
77
+
78
+ cfg_filename = f"trainer_config_{pdf_id}__{template_id}.json"
79
+ cfg_bytes = json.dumps(config_obj, indent=2).encode("utf-8")
80
+
81
+ attachments = [(cfg_filename, cfg_bytes)]
82
+
83
+ # Attach PDF if available
84
+ pdf_path = PDF_DIR / f"{pdf_id}.pdf"
85
+ pdf_name_path = PDF_DIR / f"{pdf_id}.name.txt"
86
+ pdf_filename = f"{pdf_id}.pdf"
87
+ if pdf_name_path.exists():
88
+ try:
89
+ pdf_filename = (pdf_name_path.read_text(encoding="utf-8") or "").strip() or pdf_filename
90
+ except Exception:
91
+ pdf_filename = f"{pdf_id}.pdf"
92
+
93
+ pdf_attached = False
94
+ if pdf_path.exists():
95
+ try:
96
+ pdf_bytes = pdf_path.read_bytes()
97
+ except Exception:
98
+ pdf_bytes = b""
99
+ if pdf_bytes and len(pdf_bytes) <= _MAX_EMAIL_ATTACHMENT_BYTES:
100
+ attachments.append((pdf_filename, pdf_bytes))
101
+ pdf_attached = True
102
+
103
+ subject = f"PDF Trainer: configuration updated ({template_id})"
104
+ body_lines = [
105
+ "Configuration has been updated.",
106
+ "",
107
+ f"template_id: {template_id}",
108
+ f"pdf_id: {pdf_id}",
109
+ "",
110
+ f"PDF attached: {'yes' if pdf_attached else 'no'}",
111
+ ]
112
+
113
+ gmail = _gmail_client()
114
+ gmail.send_email(
115
+ to_email=notify_to,
116
+ from_email=notify_from,
117
+ subject=subject,
118
+ body_text="\n".join(body_lines) + "\n",
119
+ attachments=attachments,
120
+ )
121
+
122
 
123
  @app.get("/health")
124
  async def health() -> Dict[str, Any]:
 
186
  pdf_id = (payload.get("pdf_id") or "").strip()
187
  template_id = (payload.get("template_id") or "").strip()
188
  config = payload.get("config")
189
+ notify_to_override = (payload.get("notify_to") or payload.get("notifyTo") or "").strip()
190
 
191
  if not pdf_id:
192
  raise HTTPException(status_code=400, detail="Missing pdf_id")
 
203
  except Exception as e:
204
  raise HTTPException(status_code=500, detail=f"failed to store config: {e}")
205
 
206
+ notify_to_default = (os.environ.get("PDF_PIPELINE_PIPELINE_NOTIFY_TO") or "").strip()
207
+ notify_to = (notify_to_override or notify_to_default).strip()
208
+
209
+ emailed = False
210
+ email_error: Optional[str] = None
211
+ if notify_to:
212
+ try:
213
+ _send_config_confirmation_email(
214
+ pdf_id=pdf_id,
215
+ template_id=template_id,
216
+ config_obj=out,
217
+ notify_to=notify_to,
218
+ )
219
+ emailed = True
220
+ except Exception as e:
221
+ email_error = str(e)
222
+
223
  pdf_exists = (PDF_DIR / f"{pdf_id}.pdf").exists()
 
224
 
225
+ message = "Configuration has been updated."
226
+ if emailed:
227
+ message += f" Confirmation email sent to {notify_to}."
228
+ elif notify_to:
229
+ message += f" Email FAILED: {email_error}"
230
+ else:
231
+ message += " (No confirmation email: set PDF_PIPELINE_PIPELINE_NOTIFY_TO.)"
232
+
233
+ return {
234
+ "ok": True,
235
+ "message": message,
236
+ "stored": str(cfg_path),
237
+ "pdf_exists": pdf_exists,
238
+ "emailed": emailed,
239
+ "notify_to": notify_to or None,
240
+ }
241
 
242
  @app.put("/api/pdf/{pdf_id}")
243
  async def put_pdf_alias(
244
  pdf_id: str,
245
  file: UploadFile = File(...),
246
+ pdf_name: Optional[str] = Form(None),
247
  ):
248
+ # Backwards-compatible alias for clients that still use PUT.
249
+ return await put_pdf(pdf_id=pdf_id, file=file, pdf_name=pdf_name)