33
44Endpoints
55---------
6- GET /api/schema Full block tree with introspected options
7- GET /api/health Liveness check; reports Athena + version info
8- POST /api/export-yaml Returns YAML content as a downloadable response
6+ GET /api/schema Full block tree with introspected options
7+ GET /api/health Liveness check; reports Athena + version info
8+ POST /api/export-yaml Returns YAML content as a downloadable response
9+ POST /api/generate-intnote Runs generateConfigInformation.py on a JSON file,
10+ compiles the resulting .tex to PDF, returns both
911"""
1012
13+ import base64
1114import copy
1215import glob
1316import logging
1417import os
18+ import shutil
19+ import subprocess
20+ import tempfile
1521
1622import yaml
1723from flask import Flask , jsonify , request , send_from_directory , Response
3238
3339_schema_cache = None
3440
41+ # Path to the ConfigDocumentation script kept from the TCT source tree
42+ _INTNOTE_SCRIPT = "/opt/TopCPToolkit/ConfigDocumentation/generateConfigInformation.py"
43+
3544
3645def _build_schema ():
3746 def enrich (block ):
@@ -64,6 +73,16 @@ def _get_tct_version():
6473 return None
6574
6675
76+ def _pdflatex_available ():
77+ """Return True if pdflatex is on PATH."""
78+ return shutil .which ("pdflatex" ) is not None
79+
80+
81+ def _intnote_available ():
82+ """Return True if both the TCT script and pdflatex are present."""
83+ return os .path .isfile (_INTNOTE_SCRIPT ) and _pdflatex_available ()
84+
85+
6786@app .before_request
6887def _warm_schema ():
6988 global _schema_cache
@@ -86,6 +105,7 @@ def health():
86105 "app_version" : APP_VERSION ,
87106 "ab_version" : _get_ab_version (),
88107 "tct_version" : _get_tct_version (),
108+ "pdflatex" : _pdflatex_available (),
89109 })
90110
91111
@@ -114,6 +134,126 @@ def export_yaml():
114134 )
115135
116136
137+ @app .route ("/api/generate-intnote" , methods = ["POST" ])
138+ def generate_intnote ():
139+ """
140+ Accept a TopCPToolkit JSON configuration file, run
141+ generateConfigInformation.py on it, compile the resulting .tex with
142+ pdflatex, and return the PDF (base64) + .tex source.
143+
144+ Form fields:
145+ json – the JSON file (required)
146+ sections – comma-separated list of sections, e.g. "muon,jet,met"
147+ (optional; omit to generate all sections)
148+ """
149+ # ── Availability checks ──────────────────────────────────────────────────
150+ if not os .path .isfile (_INTNOTE_SCRIPT ):
151+ return jsonify ({
152+ "error" : "TopCPToolkit not available — generateConfigInformation.py not found. "
153+ "Rebuild the image with TCT_VERSION set." ,
154+ }), 400
155+
156+ if not _pdflatex_available ():
157+ return jsonify ({
158+ "error" : "pdflatex not found. Rebuild the image with texlive installed." ,
159+ }), 400
160+
161+ # ── Input validation ─────────────────────────────────────────────────────
162+ json_file = request .files .get ("json" )
163+ if not json_file :
164+ return jsonify ({"error" : "No JSON file provided" }), 400
165+
166+ sections = request .form .get ("sections" , "" ).strip ()
167+
168+ # ── Run in a temp directory ──────────────────────────────────────────────
169+ with tempfile .TemporaryDirectory () as tmpdir :
170+ json_path = os .path .join (tmpdir , "config.json" )
171+ tex_path = os .path .join (tmpdir , "output.tex" )
172+ json_file .save (json_path )
173+
174+ # 1. Run the TopCPToolkit script
175+ cmd = ["python3" , _INTNOTE_SCRIPT , json_path , "-o" , tex_path ]
176+ if sections :
177+ cmd += ["--sections" , sections ]
178+
179+ logger .info ("Running: %s" , " " .join (cmd ))
180+ try :
181+ script_result = subprocess .run (
182+ cmd ,
183+ capture_output = True ,
184+ text = True ,
185+ timeout = 120 ,
186+ cwd = tmpdir ,
187+ )
188+ except subprocess .TimeoutExpired :
189+ return jsonify ({"error" : "Script timed out after 120 s" }), 400
190+
191+ script_stdout = script_result .stdout
192+ script_stderr = script_result .stderr
193+
194+ if script_result .returncode != 0 :
195+ return jsonify ({
196+ "error" : "generateConfigInformation.py exited with an error" ,
197+ "stdout" : script_stdout ,
198+ "stderr" : script_stderr ,
199+ }), 400
200+
201+ if not os .path .exists (tex_path ):
202+ return jsonify ({
203+ "error" : "Script succeeded but produced no output file" ,
204+ "stdout" : script_stdout ,
205+ "stderr" : script_stderr ,
206+ }), 400
207+
208+ with open (tex_path , encoding = "utf-8" , errors = "replace" ) as fh :
209+ tex_content = fh .read ()
210+
211+ # 2. Compile to PDF with pdflatex
212+ logger .info ("Compiling PDF with pdflatex…" )
213+ try :
214+ pdf_result = subprocess .run (
215+ [
216+ "pdflatex" ,
217+ "-interaction=nonstopmode" ,
218+ "-output-directory" , tmpdir ,
219+ tex_path ,
220+ ],
221+ capture_output = True ,
222+ text = True ,
223+ timeout = 120 ,
224+ cwd = tmpdir ,
225+ )
226+ except subprocess .TimeoutExpired :
227+ return jsonify ({
228+ "error" : "pdflatex timed out after 120 s" ,
229+ "tex" : tex_content ,
230+ "stdout" : script_stdout ,
231+ "stderr" : script_stderr ,
232+ }), 400
233+
234+ # pdflatex names the PDF after the input filename (output.pdf)
235+ pdf_path = os .path .join (tmpdir , "output.pdf" )
236+
237+ if not os .path .exists (pdf_path ):
238+ return jsonify ({
239+ "error" : "pdflatex failed to produce a PDF" ,
240+ "tex" : tex_content ,
241+ "stdout" : script_stdout ,
242+ "stderr" : script_stderr ,
243+ "pdf_log" : pdf_result .stdout + "\n " + pdf_result .stderr ,
244+ }), 400
245+
246+ with open (pdf_path , "rb" ) as fh :
247+ pdf_bytes = fh .read ()
248+
249+ return jsonify ({
250+ "pdf" : base64 .b64encode (pdf_bytes ).decode (),
251+ "tex" : tex_content ,
252+ "stdout" : script_stdout ,
253+ "stderr" : script_stderr ,
254+ })
255+
256+
117257@app .route ("/" , defaults = {"path" : "" })
118258@app .route ("/<path:path>" )
119259def serve_frontend (path ):
0 commit comments